mirror of
https://github.com/soypat/lneto.git
synced 2026-08-20 22:49:11 +00:00
Further improvements to httphi API (#173)
* add Exchange.WriteBodyString * Mux.MaxPathValues and other improvements * Mux PathValue improvemnt and fixes * MuxSlice more method muxing improvements * diagram out interesting approach to form parsing for clanker * refactor RequestParseForm and achieve greatness in API design * explicit naming of headerCapacityKV value in kvBuffer.Reset * fix Mux bug not matching paths correctly; httpraw HTTP V1 naming applied * rename many examples,use httphi in examples,remove useless maxAwaitingConn field * add ipv4.String * add ipv4 UnspecifiedAddr and BroadcastAddr * add ethernet.String
This commit is contained in:
+301
-8
@@ -54,7 +54,7 @@ func TestSetPathValues(t *testing.T) {
|
||||
{pattern: "/a/{x}/b", path: "/a//b", match: false},
|
||||
} {
|
||||
t.Run(test.pattern+"__"+test.path, func(t *testing.T) {
|
||||
vals := make([]pathValue, 8)
|
||||
vals := make([]PathValue, 8)
|
||||
match, tooShort := SetPathValues(vals, test.pattern, []byte(test.path))
|
||||
if tooShort {
|
||||
t.Fatal("8 slots must be enough for these patterns")
|
||||
@@ -76,7 +76,7 @@ func TestSetPathValues(t *testing.T) {
|
||||
// exchange owns that memory and a copy would allocate per request.
|
||||
func TestSetPathValuesAliasesRequestBuffer(t *testing.T) {
|
||||
path := []byte("/users/42/edit")
|
||||
vals := make([]pathValue, 4)
|
||||
vals := make([]PathValue, 4)
|
||||
match, _ := SetPathValues(vals, "/users/{id}/edit", path)
|
||||
if !match {
|
||||
t.Fatal("want match")
|
||||
@@ -94,7 +94,7 @@ func TestSetPathValuesAliasesRequestBuffer(t *testing.T) {
|
||||
// A destination too small to hold every wildcard must say so rather than bind a
|
||||
// partial set or write out of range.
|
||||
func TestSetPathValuesSliceTooShort(t *testing.T) {
|
||||
match, tooShort := SetPathValues(make([]pathValue, 1), "/{a}/{b}", []byte("/x/y"))
|
||||
match, tooShort := SetPathValues(make([]PathValue, 1), "/{a}/{b}", []byte("/x/y"))
|
||||
if !tooShort {
|
||||
t.Error("want pathValSliceTooShort for 2 wildcards in 1 slot")
|
||||
}
|
||||
@@ -112,11 +112,11 @@ func TestSetPathValuesSliceTooShort(t *testing.T) {
|
||||
// segment, so "/users/x%2Fy" binds id="x/y" there and id="x%2Fy" here. Matching
|
||||
// agrees either way; only the bound bytes differ.
|
||||
func TestSetPathValuesEscaping(t *testing.T) {
|
||||
vals := make([]pathValue, 4)
|
||||
vals := make([]PathValue, 4)
|
||||
if match, _ := SetPathValues(vals, "/a%2Fb/{x}", []byte("/a%2Fb/v")); !match {
|
||||
t.Error("want literal escape in pattern to match the same bytes in path")
|
||||
}
|
||||
vals = make([]pathValue, 4)
|
||||
vals = make([]PathValue, 4)
|
||||
match, _ := SetPathValues(vals, "/users/{id}", []byte("/users/x%2Fy"))
|
||||
if !match {
|
||||
t.Fatal("want match")
|
||||
@@ -128,7 +128,7 @@ func TestSetPathValuesEscaping(t *testing.T) {
|
||||
|
||||
// renderPathValues joins the bound pairs for comparison, stopping at the first
|
||||
// unused slot.
|
||||
func renderPathValues(vals []pathValue) string {
|
||||
func renderPathValues(vals []PathValue) string {
|
||||
var sb strings.Builder
|
||||
for _, v := range vals {
|
||||
if v.Key == "" {
|
||||
@@ -147,7 +147,7 @@ func renderPathValues(vals []pathValue) string {
|
||||
// Matching a request must not allocate: keys alias the mux's pattern and values
|
||||
// alias the request buffer, so nothing is copied per request.
|
||||
func TestSetPathValuesNoAlloc(t *testing.T) {
|
||||
vals := make([]pathValue, 8)
|
||||
vals := make([]PathValue, 8)
|
||||
path := []byte("/b/bk/o/a/b/c")
|
||||
allocs := testing.AllocsPerRun(100, func() {
|
||||
SetPathValues(vals, "/b/{bucket}/o/{obj...}", path)
|
||||
@@ -164,7 +164,11 @@ type pathValueMux struct {
|
||||
handler HandlerFunc
|
||||
}
|
||||
|
||||
func (m *pathValueMux) LookupHandler(method Method, path []byte, dst []pathValue) (string, HandlerFunc) {
|
||||
func (m *pathValueMux) MaxPathValues() int {
|
||||
return -1
|
||||
}
|
||||
|
||||
func (m *pathValueMux) LookupHandler(method Method, path []byte, dst []PathValue) (string, HandlerFunc) {
|
||||
if ok, _ := SetPathValues(dst, m.pattern, path); ok {
|
||||
return m.pattern, m.handler
|
||||
}
|
||||
@@ -220,3 +224,292 @@ func TestExchangePathValueClearedBetweenRequests(t *testing.T) {
|
||||
t.Errorf("want no path value on a literal route, got id=%q from the previous request", leaked)
|
||||
}
|
||||
}
|
||||
|
||||
// A lookup tries each endpoint in turn and [SetPathValues] binds as it walks, so
|
||||
// a pattern that binds values and then fails must not leave them behind for the
|
||||
// pattern that does match: a handler would read a wildcard no matched pattern has.
|
||||
func TestMuxSliceNoStaleBindingsAcrossCandidates(t *testing.T) {
|
||||
var sm MuxSlice
|
||||
sm.Reset(2)
|
||||
sm.Handle("/a/{x}/{y}/z", func(ex *Exchange) { t.Error("non-matching handler ran") })
|
||||
var gotP, gotX, gotY string
|
||||
sm.Handle("/a/{p}/b", func(ex *Exchange) {
|
||||
gotP = string(ex.PathValue("p"))
|
||||
gotX = string(ex.PathValue("x"))
|
||||
gotY = string(ex.PathValue("y"))
|
||||
ex.WriteHeader(200)
|
||||
})
|
||||
|
||||
exch := new(Exchange)
|
||||
exch.Configure(ExchangeConfig{
|
||||
RawBuf: make([]byte, 2048), RequestBufferLim: 1024,
|
||||
NumHeaderKVCap: defaultNumHeaderKVCap, MaxPathValues: sm.MaxPathValues(),
|
||||
})
|
||||
conn := newConn("GET /a/1/b HTTP/1.1\r\nHost: h\r\n\r\n")
|
||||
conn.Hangup()
|
||||
if !exch.Acquire(conn) {
|
||||
t.Fatal("fresh exchange failed to acquire")
|
||||
}
|
||||
if err := Handle(exch, &sm, nopBackoff); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotP != "1" {
|
||||
t.Errorf("want p=1 from the matched pattern, got %q", gotP)
|
||||
}
|
||||
if gotX != "" || gotY != "" {
|
||||
t.Errorf("want no x/y from the failed candidate, got x=%q y=%q", gotX, gotY)
|
||||
}
|
||||
}
|
||||
|
||||
// A pattern ending in '/' carries no brace but is still a wildcard: the trailing
|
||||
// slash is an anonymous "{...}", see [SetPathValues]. MuxSlice must route it
|
||||
// through the same matcher rather than comparing the path literally.
|
||||
func TestMuxSliceTrailingSlashPattern(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
pattern string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{pattern: "/files/", path: "/files/a/b", want: true},
|
||||
{pattern: "/files/", path: "/files/", want: true},
|
||||
{pattern: "/files/", path: "/files", want: false},
|
||||
{pattern: "/files/", path: "/other/a", want: false},
|
||||
{pattern: "/", path: "/anything/at/all", want: true},
|
||||
{pattern: "/", path: "/", want: true},
|
||||
// Without the trailing slash a pattern stays literal.
|
||||
{pattern: "/files", path: "/files/a", want: false},
|
||||
{pattern: "/files", path: "/files", want: true},
|
||||
} {
|
||||
t.Run(test.pattern+"__"+test.path, func(t *testing.T) {
|
||||
var sm MuxSlice
|
||||
sm.Reset(1)
|
||||
var served bool
|
||||
sm.Handle(test.pattern, func(ex *Exchange) { served = true; ex.WriteHeader(200) })
|
||||
// MuxSlice must agree with the matcher it delegates to.
|
||||
if ok, _ := SetPathValues(nil, test.pattern, []byte(test.path)); ok != test.want {
|
||||
t.Fatalf("SetPathValues disagrees with the table: got %v", ok)
|
||||
}
|
||||
exch := new(Exchange)
|
||||
exch.Configure(ExchangeConfig{
|
||||
RawBuf: make([]byte, 2048), RequestBufferLim: 1024,
|
||||
NumHeaderKVCap: defaultNumHeaderKVCap, MaxPathValues: sm.MaxPathValues(),
|
||||
})
|
||||
conn := newConn("GET " + test.path + " HTTP/1.1\r\nHost: h\r\n\r\n")
|
||||
conn.Hangup()
|
||||
if !exch.Acquire(conn) {
|
||||
t.Fatal("fresh exchange failed to acquire")
|
||||
}
|
||||
if err := Handle(exch, &sm, nopBackoff); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if served != test.want {
|
||||
t.Errorf("want served=%v, got %v", test.want, served)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// A malformed registration is a programming error, and one that otherwise costs
|
||||
// a permanent silent 404 at runtime: "Get /x" parses to MethUnknown, which no
|
||||
// GET request ever matches but every extension-method request does. Fail at
|
||||
// registration, where the stack points at the offending line.
|
||||
func TestMuxSliceHandlePanicsOnBadRegistration(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
reg string
|
||||
}{
|
||||
{name: "lowercase method", reg: "Get /x"},
|
||||
{name: "all lower method", reg: "get /x"},
|
||||
{name: "mixed case method", reg: "pOsT /x"},
|
||||
{name: "no leading slash", reg: "GET x"},
|
||||
{name: "bare path no slash", reg: "x"},
|
||||
{name: "empty path after method", reg: "GET "},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var sm MuxSlice
|
||||
sm.Reset(1)
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Errorf("want panic registering %q", test.reg)
|
||||
}
|
||||
}()
|
||||
sm.Handle(test.reg, func(ex *Exchange) {})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An exact duplicate is unreachable code: the first registration always wins.
|
||||
func TestMuxSliceHandlePanicsOnDuplicate(t *testing.T) {
|
||||
var sm MuxSlice
|
||||
sm.Reset(2)
|
||||
sm.Handle("GET /x", func(ex *Exchange) {})
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Error("want panic registering the same method and path twice")
|
||||
}
|
||||
}()
|
||||
sm.Handle("GET /x", func(ex *Exchange) {})
|
||||
}
|
||||
|
||||
// Extension methods are legal and uppercase, so they must still register: only
|
||||
// the case-mangled forms are rejected.
|
||||
func TestMuxSliceHandleAllowsExtensionMethod(t *testing.T) {
|
||||
var sm MuxSlice
|
||||
sm.Reset(2)
|
||||
sm.Handle("PROPFIND /dav", func(ex *Exchange) {})
|
||||
sm.Handle("/any-method", func(ex *Exchange) {}) // Bare path matches any method.
|
||||
if sm.MaxPathValues() != 0 {
|
||||
t.Errorf("want 0 path values, got %d", sm.MaxPathValues())
|
||||
}
|
||||
}
|
||||
|
||||
// A catch-all registered before the endpoints it sits above must not swallow
|
||||
// them. "/" is a wildcard pattern: its trailing slash is an anonymous "{...}",
|
||||
// so a purely first-match-wins scan hands every request to it and the specific
|
||||
// registrations below become dead code, answered with the root page instead of
|
||||
// their own body. Registering the site root first is the ordinary way to write
|
||||
// a mux, so the more specific pattern has to win regardless of order, as in
|
||||
// http.ServeMux.
|
||||
func TestMuxSliceSpecificPatternBeatsEarlierCatchAll(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
register []string
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "root registered first",
|
||||
register: []string{"/", "/hello", "/cnt", "/6"},
|
||||
path: "/cnt",
|
||||
want: "/cnt",
|
||||
}, {
|
||||
name: "root registered last",
|
||||
register: []string{"/hello", "/cnt", "/6", "/"},
|
||||
path: "/cnt",
|
||||
want: "/cnt",
|
||||
}, {
|
||||
name: "root still serves the root path",
|
||||
register: []string{"/", "/cnt"},
|
||||
path: "/",
|
||||
want: "/",
|
||||
}, {
|
||||
name: "root still catches the unregistered",
|
||||
register: []string{"/", "/cnt"},
|
||||
path: "/nowhere",
|
||||
want: "/",
|
||||
}, {
|
||||
name: "subtree wildcard loses to its own literal",
|
||||
register: []string{"/files/", "/files/index"},
|
||||
path: "/files/index",
|
||||
want: "/files/index",
|
||||
}, {
|
||||
name: "subtree wildcard keeps the rest",
|
||||
register: []string{"/files/", "/files/index"},
|
||||
path: "/files/a/b",
|
||||
want: "/files/",
|
||||
}, {
|
||||
name: "longer literal prefix wins over shorter subtree",
|
||||
register: []string{"/", "/files/", "/files/a/b"},
|
||||
path: "/files/a/b",
|
||||
want: "/files/a/b",
|
||||
}, {
|
||||
name: "named wildcard loses to the literal it covers",
|
||||
register: []string{"/users/{id}", "/users/me"},
|
||||
path: "/users/me",
|
||||
want: "/users/me",
|
||||
}, {
|
||||
name: "named wildcard keeps everything else",
|
||||
register: []string{"/users/{id}", "/users/me"},
|
||||
path: "/users/42",
|
||||
want: "/users/{id}",
|
||||
},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var sm MuxSlice
|
||||
sm.Reset(len(test.register))
|
||||
for _, pattern := range test.register {
|
||||
sm.Handle(pattern, func(ex *Exchange) { ex.WriteHeader(200) })
|
||||
}
|
||||
pathVals := make([]PathValue, max(sm.MaxPathValues(), 0))
|
||||
got, handler := sm.LookupHandler(MethGet, []byte(test.path), pathVals)
|
||||
if handler == nil {
|
||||
t.Fatalf("%s matched no handler, want %q", test.path, test.want)
|
||||
}
|
||||
if got != test.want {
|
||||
t.Errorf("%s matched pattern %q, want %q", test.path, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// pathVals sizes the slice SetPathValues writes into, so it must count exactly
|
||||
// the wildcards that bind. Counting braces over-reports: "{$}" marks the path's
|
||||
// end, an anonymous "{...}" has no name, and a brace inside a literal segment is
|
||||
// not a wildcard at all. Each of those binds nothing.
|
||||
func TestMuxSliceMaxPathValuesCountsOnlyBindingWildcards(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
pattern string
|
||||
want int
|
||||
}{
|
||||
{pattern: "/health", want: 0},
|
||||
{pattern: "/", want: 0},
|
||||
{pattern: "/files/", want: 0}, // Anonymous trailing wildcard.
|
||||
{pattern: "/{$}", want: 0}, // End-of-path marker.
|
||||
{pattern: "/a/{$}", want: 0}, //
|
||||
{pattern: "/b_{bucket}", want: 0}, // Literal: brace not a whole segment.
|
||||
{pattern: "/{...}", want: 0}, // Multi wildcard with no name.
|
||||
{pattern: "/users/{id}", want: 1}, //
|
||||
{pattern: "/files/{p...}", want: 1}, //
|
||||
{pattern: "/{a}/{b}", want: 2}, //
|
||||
{pattern: "/b/{bucket}/o/{obj...}", want: 2},
|
||||
} {
|
||||
t.Run(test.pattern, func(t *testing.T) {
|
||||
var sm MuxSlice
|
||||
sm.Reset(1)
|
||||
sm.Handle(test.pattern, func(ex *Exchange) {})
|
||||
if got := sm.MaxPathValues(); got != test.want {
|
||||
t.Errorf("want %d path values, got %d", test.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Sizing and routing are different questions: "{$}" binds no values yet still
|
||||
// needs the matcher, so an exact pathVals count must not send it to the literal
|
||||
// comparison instead.
|
||||
func TestMuxSliceZeroValueWildcardStillMatches(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
pattern string
|
||||
path string
|
||||
want bool
|
||||
}{
|
||||
{pattern: "/{$}", path: "/", want: true},
|
||||
{pattern: "/{$}", path: "/x", want: false},
|
||||
{pattern: "/a/{$}", path: "/a/", want: true},
|
||||
{pattern: "/a/{$}", path: "/a/b", want: false},
|
||||
{pattern: "/{...}", path: "/any/thing", want: true},
|
||||
} {
|
||||
t.Run(test.pattern+"__"+test.path, func(t *testing.T) {
|
||||
var sm MuxSlice
|
||||
sm.Reset(1)
|
||||
var served bool
|
||||
sm.Handle(test.pattern, func(ex *Exchange) { served = true; ex.WriteHeader(200) })
|
||||
exch := new(Exchange)
|
||||
exch.Configure(ExchangeConfig{
|
||||
RawBuf: make([]byte, 2048), RequestBufferLim: 1024,
|
||||
NumHeaderKVCap: defaultNumHeaderKVCap, MaxPathValues: sm.MaxPathValues(),
|
||||
})
|
||||
conn := newConn("GET " + test.path + " HTTP/1.1\r\nHost: h\r\n\r\n")
|
||||
conn.Hangup()
|
||||
if !exch.Acquire(conn) {
|
||||
t.Fatal("acquire")
|
||||
}
|
||||
if err := Handle(exch, &sm, nopBackoff); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if served != test.want {
|
||||
t.Errorf("want served=%v, got %v", test.want, served)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user