Skip to content

Commit 519e7f4

Browse files
authored
Fix verb parsing (#1947)
* Start variable/verb fix * Add tests for parser side * Update test * Adjust runtime logic to use the verb in the pattern * Remove now-unneeded test
1 parent 3c4b5a0 commit 519e7f4

File tree

4 files changed

+116
-28
lines changed

4 files changed

+116
-28
lines changed

internal/httprule/parse.go

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ func tokenize(path string) (tokens []string, verb string) {
4545
field
4646
nested
4747
)
48-
var (
49-
st = init
50-
)
48+
st := init
5149
for path != "" {
5250
var idx int
5351
switch st {
@@ -80,8 +78,30 @@ func tokenize(path string) (tokens []string, verb string) {
8078
}
8179

8280
l := len(tokens)
81+
// See
82+
// https://github.com/grpc-ecosystem/grpc-gateway/pull/1947#issuecomment-774523693 ;
83+
// although normal and backwards-compat logic here is to use the last index
84+
// of a colon, if the final segment is a variable followed by a colon, the
85+
// part following the colon must be a verb. Hence if the previous token is
86+
// an end var marker, we switch the index we're looking for to Index instead
87+
// of LastIndex, so that we correctly grab the remaining part of the path as
88+
// the verb.
89+
var penultimateTokenIsEndVar bool
90+
switch l {
91+
case 0, 1:
92+
// Not enough to be variable so skip this logic and don't result in an
93+
// invalid index
94+
default:
95+
penultimateTokenIsEndVar = tokens[l-2] == "}"
96+
}
8397
t := tokens[l-1]
84-
if idx := strings.LastIndex(t, ":"); idx == 0 {
98+
var idx int
99+
if penultimateTokenIsEndVar {
100+
idx = strings.Index(t, ":")
101+
} else {
102+
idx = strings.LastIndex(t, ":")
103+
}
104+
if idx == 0 {
85105
tokens, verb = tokens[:l-1], t[1:]
86106
} else if idx > 0 {
87107
tokens[l-1], verb = t[:idx], t[idx+1:]

internal/httprule/parse_test.go

Lines changed: 42 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ func TestTokenize(t *testing.T) {
1313
for _, spec := range []struct {
1414
src string
1515
tokens []string
16+
verb string
1617
}{
1718
{
1819
src: "",
@@ -81,22 +82,51 @@ func TestTokenize(t *testing.T) {
8182
eof,
8283
},
8384
},
85+
{
86+
src: "v1/a/{endpoint}:a",
87+
tokens: []string{
88+
"v1", "/",
89+
"a", "/",
90+
"{", "endpoint", "}",
91+
eof,
92+
},
93+
verb: "a",
94+
},
95+
{
96+
src: "v1/a/{endpoint}:b:c",
97+
tokens: []string{
98+
"v1", "/",
99+
"a", "/",
100+
"{", "endpoint", "}",
101+
eof,
102+
},
103+
verb: "b:c",
104+
},
84105
} {
85106
tokens, verb := tokenize(spec.src)
86107
if got, want := tokens, spec.tokens; !reflect.DeepEqual(got, want) {
87108
t.Errorf("tokenize(%q) = %q, _; want %q, _", spec.src, got, want)
88109
}
89-
if got, want := verb, ""; got != want {
90-
t.Errorf("tokenize(%q) = _, %q; want _, %q", spec.src, got, want)
91-
}
92110

93-
src := fmt.Sprintf("%s:%s", spec.src, "LOCK")
94-
tokens, verb = tokenize(src)
95-
if got, want := tokens, spec.tokens; !reflect.DeepEqual(got, want) {
96-
t.Errorf("tokenize(%q) = %q, _; want %q, _", src, got, want)
97-
}
98-
if got, want := verb, "LOCK"; got != want {
99-
t.Errorf("tokenize(%q) = _, %q; want _, %q", src, got, want)
111+
switch {
112+
case spec.verb != "":
113+
if got, want := verb, spec.verb; !reflect.DeepEqual(got, want) {
114+
t.Errorf("tokenize(%q) = %q, _; want %q, _", spec.src, got, want)
115+
}
116+
117+
default:
118+
if got, want := verb, ""; got != want {
119+
t.Errorf("tokenize(%q) = _, %q; want _, %q", spec.src, got, want)
120+
}
121+
122+
src := fmt.Sprintf("%s:%s", spec.src, "LOCK")
123+
tokens, verb = tokenize(src)
124+
if got, want := tokens, spec.tokens; !reflect.DeepEqual(got, want) {
125+
t.Errorf("tokenize(%q) = %q, _; want %q, _", src, got, want)
126+
}
127+
if got, want := verb, "LOCK"; got != want {
128+
t.Errorf("tokenize(%q) = _, %q; want _, %q", src, got, want)
129+
}
100130
}
101131
}
102132
}
@@ -209,7 +239,8 @@ func TestParseSegments(t *testing.T) {
209239
"a", "/", "b", "/", "*", "/", "c",
210240
"}", "/",
211241
"**",
212-
eof},
242+
eof,
243+
},
213244
want: []segment{
214245
literal("v1"),
215246
variable{

runtime/mux.go

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -205,18 +205,6 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
205205
}
206206

207207
components := strings.Split(path[1:], "/")
208-
l := len(components)
209-
var verb string
210-
idx := strings.LastIndex(components[l-1], ":")
211-
if idx == 0 {
212-
_, outboundMarshaler := MarshalerForRequest(s, r)
213-
s.routingErrorHandler(ctx, s, outboundMarshaler, w, r, http.StatusNotFound)
214-
return
215-
}
216-
if idx > 0 {
217-
c := components[l-1]
218-
components[l-1], verb = c[:idx], c[idx+1:]
219-
}
220208

221209
if override := r.Header.Get("X-HTTP-Method-Override"); override != "" && s.isPathLengthFallback(r) {
222210
r.Method = strings.ToUpper(override)
@@ -227,7 +215,35 @@ func (s *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
227215
return
228216
}
229217
}
218+
219+
// Verb out here is to memoize for the fallback case below
220+
var verb string
221+
230222
for _, h := range s.handlers[r.Method] {
223+
// If the pattern has a verb, explicitly look for a suffix in the last
224+
// component that matches a colon plus the verb. This allows us to
225+
// handle some cases that otherwise can't be correctly handled by the
226+
// former LastIndex case, such as when the verb literal itself contains
227+
// a colon. This should work for all cases that have run through the
228+
// parser because we know what verb we're looking for, however, there
229+
// are still some cases that the parser itself cannot disambiguate. See
230+
// the comment there if interested.
231+
patVerb := h.pat.Verb()
232+
l := len(components)
233+
lastComponent := components[l-1]
234+
var idx int = -1
235+
if patVerb != "" && strings.HasSuffix(lastComponent, ":"+patVerb) {
236+
idx = len(lastComponent) - len(patVerb) - 1
237+
}
238+
if idx == 0 {
239+
_, outboundMarshaler := MarshalerForRequest(s, r)
240+
s.routingErrorHandler(ctx, s, outboundMarshaler, w, r, http.StatusNotFound)
241+
return
242+
}
243+
if idx > 0 {
244+
components[l-1], verb = lastComponent[:idx], lastComponent[idx+1:]
245+
}
246+
231247
pathParams, err := h.pat.Match(components, verb)
232248
if err != nil {
233249
continue

runtime/mux_test.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,28 @@ func TestMuxServeHTTP(t *testing.T) {
308308
},
309309
respStatus: http.StatusBadRequest,
310310
},
311+
{
312+
patterns: []stubPattern{
313+
{
314+
method: "POST",
315+
ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1},
316+
pool: []string{"foo", "id"},
317+
},
318+
{
319+
method: "POST",
320+
ops: []int{int(utilities.OpLitPush), 0, int(utilities.OpPush), 0, int(utilities.OpConcatN), 1, int(utilities.OpCapture), 1},
321+
pool: []string{"foo", "id"},
322+
verb: "verb:subverb",
323+
},
324+
},
325+
reqMethod: "POST",
326+
reqPath: "/foo/bar:verb:subverb",
327+
headers: map[string]string{
328+
"Content-Type": "application/json",
329+
},
330+
respStatus: http.StatusOK,
331+
respContent: "POST /foo/{id=*}:verb:subverb",
332+
},
311333
} {
312334
t.Run(strconv.Itoa(i), func(t *testing.T) {
313335
var opts []runtime.ServeMuxOption
@@ -437,5 +459,4 @@ func TestServeMux_HandlePath(t *testing.T) {
437459
}
438460
})
439461
}
440-
441462
}

0 commit comments

Comments
 (0)