net/http: implement path value methods on Request

Add Request.PathValue and Request.SetPathValue,
and the fields on Request required to support them.

Populate those fields in ServeMux.ServeHTTP.

Updates #61410.

Change-Id: Ic88cb865b0d865a30d3b35ece8e0382c58ef67d1
Reviewed-on: https://go-review.googlesource.com/c/go/+/528355
Run-TryBot: Jonathan Amsterdam <jba@google.com>
Reviewed-by: Damien Neil <dneil@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
Jonathan Amsterdam 2023-09-13 16:58:24 -04:00
parent fccd0b9b70
commit 495830acd6
5 changed files with 154 additions and 13 deletions

2
api/next/61410.txt Normal file
View File

@ -0,0 +1,2 @@
pkg net/http, method (*Request) PathValue(string) string #61410
pkg net/http, method (*Request) SetPathValue(string, string) #61410

View File

@ -329,6 +329,11 @@ type Request struct {
// It is unexported to prevent people from using Context wrong
// and mutating the contexts held by callers of the same request.
ctx context.Context
// The following fields are for requests matched by ServeMux.
pat *pattern // the pattern that matched
matches []string // values for the matching wildcards in pat
otherValues map[string]string // for calls to SetPathValue that don't match a wildcard
}
// Context returns the request's context. To change the context, use
@ -1415,6 +1420,48 @@ func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, e
return nil, nil, ErrMissingFile
}
// PathValue returns the value for the named path wildcard in the ServeMux pattern
// that matched the request.
// It returns the empty string if the request was not matched against a pattern
// or there is no such wildcard in the pattern.
func (r *Request) PathValue(name string) string {
if i := r.patIndex(name); i >= 0 {
return r.matches[i]
}
return r.otherValues[name]
}
func (r *Request) SetPathValue(name, value string) {
if i := r.patIndex(name); i >= 0 {
r.matches[i] = value
} else {
if r.otherValues == nil {
r.otherValues = map[string]string{}
}
r.otherValues[name] = value
}
}
// patIndex returns the index of name in the list of named wildcards of the
// request's pattern, or -1 if there is no such name.
func (r *Request) patIndex(name string) int {
// The linear search seems expensive compared to a map, but just creating the map
// takes a lot of time, and most patterns will just have a couple of wildcards.
if r.pat == nil {
return -1
}
i := 0
for _, seg := range r.pat.segments {
if seg.wild && seg.s != "" {
if name == seg.s {
return i
}
i++
}
}
return -1
}
func (r *Request) expectsContinue() bool {
return hasToken(r.Header.get("Expect"), "100-continue")
}

View File

@ -16,6 +16,7 @@ import (
"math"
"mime/multipart"
. "net/http"
"net/http/httptest"
"net/url"
"os"
"reflect"
@ -1414,3 +1415,92 @@ func TestErrNotSupported(t *testing.T) {
t.Error("errors.Is(ErrNotSupported, errors.ErrUnsupported) failed")
}
}
func TestPathValueNoMatch(t *testing.T) {
// Check that PathValue and SetPathValue work on a Request that was never matched.
var r Request
if g, w := r.PathValue("x"), ""; g != w {
t.Errorf("got %q, want %q", g, w)
}
r.SetPathValue("x", "a")
if g, w := r.PathValue("x"), "a"; g != w {
t.Errorf("got %q, want %q", g, w)
}
}
func TestPathValue(t *testing.T) {
for _, test := range []struct {
pattern string
url string
want map[string]string
}{
{
"/{a}/is/{b}/{c...}",
"/now/is/the/time/for/all",
map[string]string{
"a": "now",
"b": "the",
"c": "time/for/all",
"d": "",
},
},
// TODO(jba): uncomment these tests when we implement path escaping (forthcoming).
// {
// "/names/{name}/{other...}",
// "/names/" + url.PathEscape("/john") + "/address",
// map[string]string{
// "name": "/john",
// "other": "address",
// },
// },
// {
// "/names/{name}/{other...}",
// "/names/" + url.PathEscape("john/doe") + "/address",
// map[string]string{
// "name": "john/doe",
// "other": "address",
// },
// },
} {
mux := NewServeMux()
mux.HandleFunc(test.pattern, func(w ResponseWriter, r *Request) {
for name, want := range test.want {
got := r.PathValue(name)
if got != want {
t.Errorf("%q, %q: got %q, want %q", test.pattern, name, got, want)
}
}
})
server := httptest.NewServer(mux)
defer server.Close()
_, err := Get(server.URL + test.url)
if err != nil {
t.Fatal(err)
}
}
}
func TestSetPathValue(t *testing.T) {
mux := NewServeMux()
mux.HandleFunc("/a/{b}/c/{d...}", func(_ ResponseWriter, r *Request) {
kvs := map[string]string{
"b": "X",
"d": "Y",
"a": "Z",
}
for k, v := range kvs {
r.SetPathValue(k, v)
}
for k, w := range kvs {
if g := r.PathValue(k); g != w {
t.Errorf("got %q, want %q", g, w)
}
}
})
server := httptest.NewServer(mux)
defer server.Close()
_, err := Get(server.URL + "/a/b/c/d/e")
if err != nil {
t.Fatal(err)
}
}

View File

@ -2410,14 +2410,15 @@ func stripHostPort(h string) string {
// If there is no registered handler that applies to the request,
// Handler returns a “page not found” handler and an empty pattern.
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
return mux.findHandler(r)
h, p, _, _ := mux.findHandler(r)
return h, p
}
// findHandler finds a handler for a request.
// If there is a matching handler, it returns it and the pattern that matched.
// Otherwise it returns a Redirect or NotFound handler with the path that would match
// after the redirect.
func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string) {
func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string, _ *pattern, matches []string) {
var n *routingNode
// TODO(jba): use escaped path. This is an independent change that is also part
// of proposal https://go.dev/issue/61410.
@ -2430,11 +2431,11 @@ func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string) {
// but the path canonicalization does not.
_, _, u := mux.matchOrRedirect(r.URL.Host, r.Method, path, r.URL)
if u != nil {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path, nil, nil
}
// Redo the match, this time with r.Host instead of r.URL.Host.
// Pass a nil URL to skip the trailing-slash redirect logic.
n, _, _ = mux.matchOrRedirect(r.Host, r.Method, path, nil)
n, matches, _ = mux.matchOrRedirect(r.Host, r.Method, path, nil)
} else {
// All other requests have any port stripped and path cleaned
// before passing to mux.handler.
@ -2444,9 +2445,9 @@ func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string) {
// If the given path is /tree and its handler is not registered,
// redirect for /tree/.
var u *url.URL
n, _, u = mux.matchOrRedirect(host, r.Method, path, r.URL)
n, matches, u = mux.matchOrRedirect(host, r.Method, path, r.URL)
if u != nil {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path, nil, nil
}
if path != r.URL.Path {
// Redirect to cleaned path.
@ -2455,14 +2456,14 @@ func (mux *ServeMux) findHandler(r *Request) (h Handler, patStr string) {
patStr = n.pattern.String()
}
u := &url.URL{Path: path, RawQuery: r.URL.RawQuery}
return RedirectHandler(u.String(), StatusMovedPermanently), patStr
return RedirectHandler(u.String(), StatusMovedPermanently), patStr, nil, nil
}
}
if n == nil {
// TODO(jba): support 405 (MethodNotAllowed) by checking for patterns with different methods.
return NotFoundHandler(), ""
return NotFoundHandler(), "", nil, nil
}
return n.handler, n.pattern.String()
return n.handler, n.pattern.String(), n.pattern, matches
}
// matchOrRedirect looks up a node in the tree that matches the host, method and path.
@ -2551,8 +2552,9 @@ func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.findHandler(r)
// TODO(jba); save matches in Request.
h, _, pat, matches := mux.findHandler(r)
r.pat = pat
r.matches = matches
h.ServeHTTP(w, r)
}

View File

@ -110,7 +110,7 @@ func TestFindHandler(t *testing.T) {
r.Method = test.method
r.Host = "example.com"
r.URL = &url.URL{Path: test.path}
gotH, _ := mux.findHandler(&r)
gotH, _, _, _ := mux.findHandler(&r)
got := fmt.Sprintf("%#v", gotH)
if got != test.wantHandler {
t.Errorf("%s %q: got %q, want %q", test.method, test.path, got, test.wantHandler)
@ -204,7 +204,7 @@ func BenchmarkServerMatch(b *testing.B) {
if err != nil {
b.Fatal(err)
}
if h, p := mux.findHandler(r); h != nil && p == "" {
if h, p, _, _ := mux.findHandler(r); h != nil && p == "" {
b.Error("impossible")
}
}