Repository: cloudwego/hertz
Pull request #1359 (2025-05-21 08:17:14)

fix(binding): use UnmarshalText for types like enum
#### What type of PR is this?
fix

<!--
Add one of the following kinds:

build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm)
ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs)
docs: Documentation only changes
feat: A new feature
optimize: A new optimization
fix: A bug fix
perf: A code change that improves performance
refactor: A code change that neither fixes a bug nor adds a feature
style: Changes that do not affect the meaning of the code (white space, formatting, missing semi-colons, etc)
test: Adding missing tests or correcting existing tests
chore: Changes to the build process or auxiliary tools and libraries such as documentation generation
-->

#### Check the PR title.
<!--
The description of the title will be attached in Release Notes, 
so please describe it from user-oriented, what this PR does / why we need it.
Please check your PR title with the below requirements:
-->
- [ ] This PR title match the format: \<type\>(optional scope): \<description\>
- [ ] The description of this PR title is user-oriented and clear enough for others to understand.
- [ ] Attach the PR updating the user documentation if the current PR requires user awareness at the usage level. [User docs repo](https://github.com/cloudwego/cloudwego.github.io)


#### (Optional) Translate the PR title into Chinese.


#### (Optional) More detailed description for this PR(en: English/zh: Chinese).
<!--
Provide more detailed info for review(e.g., it's recommended to provide perf data if this is a perf type PR).
-->
en: 
for enum type we generate `String()` and use it by default for sending requests. 
if users also use the type for binding, it will fail with `strconv.ParseInt` err.
This change make use of the generated `UnmarshalText` func and fall back to `strconv.ParseInt` if any err.

zh(optional):


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/cloudwego/hertz/pull/1359?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) Report
Attention: Patch coverage is `0%` with `21 lines` in your changes missing coverage. Please review.
> Project coverage is 73.61%. Comparing base [(`3835def`)](https://app.codecov.io/gh/cloudwego/hertz/commit/3835def0a0fc229fde1fbd20b060b53ca87efdfc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) to head [(`97edd34`)](https://app.codecov.io/gh/cloudwego/hertz/commit/97edd34374e4ffc4226c9ad096587d77f3d6cc5b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).

| [Files with missing lines](https://app.codecov.io/gh/cloudwego/hertz/pull/1359?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) | Patch % | Lines |
|---|---|---|
| [pkg/app/server/binding/internal/decoder/util.go](https://app.codecov.io/gh/cloudwego/hertz/pull/1359?src=pr&el=tree&filepath=pkg%2Fapp%2Fserver%2Fbinding%2Finternal%2Fdecoder%2Futil.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2FwcC9zZXJ2ZXIvYmluZGluZy9pbnRlcm5hbC9kZWNvZGVyL3V0aWwuZ28=) | 0.00% | [15 Missing :warning: ](https://app.codecov.io/gh/cloudwego/hertz/pull/1359?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) |
| [...rver/binding/internal/decoder/base\_type\_decoder.go](https://app.codecov.io/gh/cloudwego/hertz/pull/1359?src=pr&el=tree&filepath=pkg%2Fapp%2Fserver%2Fbinding%2Finternal%2Fdecoder%2Fbase_type_decoder.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego#diff-cGtnL2FwcC9zZXJ2ZXIvYmluZGluZy9pbnRlcm5hbC9kZWNvZGVyL2Jhc2VfdHlwZV9kZWNvZGVyLmdv) | 0.00% | [6 Missing :warning: ](https://app.codecov.io/gh/cloudwego/hertz/pull/1359?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##           develop    #1359      +/-   ##
===========================================
- Coverage    73.70%   73.61%   -0.09%     
===========================================
  Files          145      145              
  Lines        16856    16871      +15     
===========================================
- Hits         12423    12420       -3     
- Misses        3844     3860      +16     
- Partials       589      591       +2     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/cloudwego/hertz/pull/1359?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cloudwego).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/pkg/app/server/binding/internal/decoder/base_type_decoder.go b/pkg/app/server/binding/internal/decoder/base_type_decoder.go
index bf26c9c..d99f90a 100644
--- a/pkg/app/server/binding/internal/decoder/base_type_decoder.go
+++ b/pkg/app/server/binding/internal/decoder/base_type_decoder.go
@@ -123,6 +123,11 @@ func (d *baseTypeFieldTextDecoder) Decode(req *protocol.Request, params param.Pa
 	}
 
 	// Non-pointer elems
+	if field.CanAddr() {
+		if tryTextUnmarshaler(field.Addr(), text) {
+			return nil
+		}
+	}
 	err = d.decoder.UnmarshalString(text, field, d.config.LooseZeroMode)
 	if err != nil {
 		return fmt.Errorf("unable to decode '%s' as %s: %w", text, d.fieldType.Name(), err)
diff --git a/pkg/app/server/binding/internal/decoder/util.go b/pkg/app/server/binding/internal/decoder/util.go
index be141c2..0464547 100644
--- a/pkg/app/server/binding/internal/decoder/util.go
+++ b/pkg/app/server/binding/internal/decoder/util.go
@@ -17,6 +17,7 @@
 package decoder
 
 import (
+	"encoding"
 	"fmt"
 	"reflect"
 	"strings"
@@ -45,8 +46,7 @@ func toDefaultValue(typ reflect.Type, defaultValue string) string {
 }
 
 // stringToValue is used to dynamically create reflect.Value for 'text'
-func stringToValue(elemType reflect.Type, text string, req *protocol.Request, params param.Params, config *DecodeConfig) (v reflect.Value, err error) {
-	v = reflect.New(elemType).Elem()
+func stringToValue(elemType reflect.Type, text string, req *protocol.Request, params param.Params, config *DecodeConfig) (reflect.Value, error) {
 	if customizedFunc, exist := config.TypeUnmarshalFuncs[elemType]; exist {
 		val, err := customizedFunc(req, params, text)
 		if err != nil {
@@ -54,23 +54,42 @@ func stringToValue(elemType reflect.Type, text string, req *protocol.Request, pa
 		}
 		return val, nil
 	}
+	v := reflect.New(elemType)
+	if tryTextUnmarshaler(v, text) {
+		return v.Elem(), nil
+	}
 	switch elemType.Kind() {
-	case reflect.Struct:
-		err = hJson.Unmarshal(bytesconv.S2b(text), v.Addr().Interface())
-	case reflect.Map:
-		err = hJson.Unmarshal(bytesconv.S2b(text), v.Addr().Interface())
+	case reflect.Struct, reflect.Map:
+		if err := hJson.Unmarshal(bytesconv.S2b(text), v.Interface()); err != nil {
+			return reflect.Value{}, err
+		}
+		return v.Elem(), nil
+
 	case reflect.Array, reflect.Slice:
 		// do nothing
+		return v.Elem(), nil
+
 	default:
 		decoder, err := SelectTextDecoder(elemType)
 		if err != nil {
-			return reflect.Value{}, fmt.Errorf("unsupported type %s for slice/array", elemType.String())
+			return reflect.Value{}, err
 		}
+		v = v.Elem()
 		err = decoder.UnmarshalString(text, v, config.LooseZeroMode)
 		if err != nil {
 			return reflect.Value{}, fmt.Errorf("unable to decode '%s' as %s: %w", text, elemType.String(), err)
 		}
+		return v, nil
 	}
 
-	return v, err
+}
+
+func tryTextUnmarshaler(v reflect.Value, s string) bool {
+	enc, ok := v.Interface().(encoding.TextUnmarshaler)
+	if ok {
+		if err := enc.UnmarshalText(bytesconv.S2b(s)); err == nil {
+			return true
+		}
+	}
+	return false
 }
diff --git a/pkg/app/server/option.go b/pkg/app/server/option.go
index 73188d1..7a77778 100644
--- a/pkg/app/server/option.go
+++ b/pkg/app/server/option.go
@@ -278,7 +278,8 @@ func WithH2C(enable bool) config.Option {
 	}}
 }
 
-// WithReadBufferSize sets the read buffer size which also limit the header size.
+// WithReadBufferSize sets the size of each read buffer node in standard transport.
+// NOTE: this cannot limit the header size.
 func WithReadBufferSize(size int) config.Option {
 	return config.Option{F: func(o *config.Options) {
 		o.ReadBufferSize = size
diff --git a/pkg/network/standard/transport.go b/pkg/network/standard/transport.go
index 1b08509..79d5ea6 100644
--- a/pkg/network/standard/transport.go
+++ b/pkg/network/standard/transport.go
@@ -31,13 +31,8 @@ import (
 )
 
 type transport struct {
-	// Per-connection buffer size for requests' reading.
-	// This also limits the maximum header size.
-	//
-	// Increase this buffer if your clients send multi-KB RequestURIs
-	// and/or multi-KB headers (for example, BIG cookies).
-	//
-	// Default buffer size is used if not set.
+	// The underlying read buffer is a buffer node list, `readBufferSize` is the size of a single node.
+	// `defaultReadBufferSize` (4KB) is used if not set.
 	readBufferSize           int
 	network                  string
 	addr                     string
diff --git a/pkg/route/engine.go b/pkg/route/engine.go
index 6589cf9..81e4f75 100644
--- a/pkg/route/engine.go
+++ b/pkg/route/engine.go
@@ -707,7 +707,7 @@ func (engine *Engine) addRoute(method, path string, handlers app.HandlersChain)
 
 	methodRouter := engine.trees.get(method)
 	if methodRouter == nil {
-		methodRouter = &router{method: method, root: &node{}, hasTsrHandler: make(map[string]bool)}
+		methodRouter = &router{method: method, root: &node{}}
 		engine.trees = append(engine.trees, methodRouter)
 	}
 	methodRouter.addRoute(path, handlers)
diff --git a/pkg/route/tree.go b/pkg/route/tree.go
index 8b63c14..14b5e7b 100644
--- a/pkg/route/tree.go
+++ b/pkg/route/tree.go
@@ -55,9 +55,8 @@ import (
 )
 
 type router struct {
-	method        string
-	root          *node
-	hasTsrHandler map[string]bool
+	method string
+	root   *node
 }
 
 type MethodTrees []*router


Test patch:
diff --git a/pkg/app/server/binding/binder_test.go b/pkg/app/server/binding/binder_test.go
index 1ea7b1e..4f5f742 100644
--- a/pkg/app/server/binding/binder_test.go
+++ b/pkg/app/server/binding/binder_test.go
@@ -41,7 +41,9 @@
 package binding
 
 import (
+	"encoding"
 	"encoding/json"
+	"errors"
 	"fmt"
 	"mime/multipart"
 	"net/url"
@@ -1665,6 +1667,41 @@ func TestBind_NormalizeContentType(t *testing.T) {
 	assert.DeepEqual(t, "version", result.Version)
 }
 
+type TestEnumType int32
+
+var _ encoding.TextUnmarshaler = (*TestEnumType)(nil)
+
+func (p *TestEnumType) UnmarshalText(v []byte) error {
+	switch string(v) {
+	case "one":
+		*p = 1
+	case "two":
+		*p = 2
+	default:
+		return errors.New("invalid")
+	}
+	return nil
+}
+
+func TestBind_TextUnmarshaler(t *testing.T) {
+	type Query struct {
+		A TestEnumType  `query:"a"`
+		B TestEnumType  `query:"b"`
+		C *TestEnumType `query:"c"`
+		D *TestEnumType `query:"d"`
+	}
+	q := &Query{}
+	req := newMockRequest().SetRequestURI("http://example.com?a=1&b=one&c=2&d=two")
+	err := DefaultBinder().BindQuery(req.Req, q)
+	assert.Nil(t, err)
+	assert.DeepEqual(t, TestEnumType(1), q.A)
+	assert.DeepEqual(t, TestEnumType(1), q.B)
+	assert.NotNil(t, q.C)
+	assert.NotNil(t, q.D)
+	assert.DeepEqual(t, TestEnumType(2), *q.C)
+	assert.DeepEqual(t, TestEnumType(2), *q.D)
+}
+
 func Benchmark_Binding(b *testing.B) {
 	type Req struct {
 		Version string `path:"v"`
diff --git a/pkg/app/server/binding/internal/decoder/util_test.go b/pkg/app/server/binding/internal/decoder/util_test.go
new file mode 100644
index 0000000..0031e58
--- /dev/null
+++ b/pkg/app/server/binding/internal/decoder/util_test.go
@@ -0,0 +1,206 @@
+/*
+ * Copyright 2024 CloudWeGo Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package decoder
+
+import (
+	"encoding"
+	"errors"
+	"reflect"
+	"testing"
+
+	"github.com/cloudwego/hertz/pkg/common/test/assert"
+	"github.com/cloudwego/hertz/pkg/protocol"
+	"github.com/cloudwego/hertz/pkg/route/param"
+)
+
+type testTextUnmarshaler struct {
+	Value string
+}
+
+func (t *testTextUnmarshaler) UnmarshalText(text []byte) error {
+	t.Value = string(text)
+	return nil
+}
+
+var _ encoding.TextUnmarshaler = (*testTextUnmarshaler)(nil)
+
+func TestStringToValue(t *testing.T) {
+	tests := []struct {
+		name        string
+		elemType    reflect.Type
+		text        string
+		config      *DecodeConfig
+		expectValue interface{}
+		expectError bool
+	}{
+		{
+			name:        "string type",
+			elemType:    reflect.TypeOf(""),
+			text:        "test string",
+			expectValue: "test string",
+		},
+		{
+			name:        "int type",
+			elemType:    reflect.TypeOf(0),
+			text:        "42",
+			expectValue: 42,
+		},
+		{
+			name:        "bool type",
+			elemType:    reflect.TypeOf(false),
+			text:        "true",
+			expectValue: true,
+		},
+		{
+			name:        "float type",
+			elemType:    reflect.TypeOf(0.0),
+			text:        "3.14",
+			expectValue: 3.14,
+		},
+		{
+			name:        "text unmarshaler",
+			elemType:    reflect.TypeOf(testTextUnmarshaler{}),
+			text:        "custom text",
+			expectValue: testTextUnmarshaler{Value: "custom text"},
+		},
+		{
+			name:        "invalid int",
+			elemType:    reflect.TypeOf(0),
+			text:        "not an int",
+			expectError: true,
+		},
+		{
+			name:        "struct type",
+			elemType:    reflect.TypeOf(struct{ Name string }{}),
+			text:        `{"Name":"test"}`,
+			expectValue: struct{ Name string }{Name: "test"},
+		},
+		{
+			name:        "struct type err",
+			elemType:    reflect.TypeOf(struct{ Name string }{}),
+			text:        `{"Name":1}`,
+			expectError: true,
+		},
+		{
+			name:        "list type",
+			elemType:    reflect.TypeOf([]int{}),
+			expectValue: *new([]int),
+		},
+		{
+			name:        "map type",
+			elemType:    reflect.TypeOf(map[string]interface{}{}),
+			text:        `{"key":"value"}`,
+			expectValue: map[string]interface{}{"key": "value"},
+		},
+		{
+			name:        "unsupported type",
+			elemType:    reflect.TypeOf(complex64(0)),
+			expectError: true,
+		},
+		{
+			name:     "custom type unmarshal func",
+			elemType: reflect.TypeOf(testTextUnmarshaler{}),
+			text:     "custom func",
+			config: &DecodeConfig{
+				TypeUnmarshalFuncs: map[reflect.Type]CustomizeDecodeFunc{
+					reflect.TypeOf(testTextUnmarshaler{}): func(req *protocol.Request, params param.Params, text string) (reflect.Value, error) {
+						return reflect.ValueOf(testTextUnmarshaler{Value: "from custom func"}), nil
+					},
+				},
+			},
+			expectValue: testTextUnmarshaler{Value: "from custom func"},
+		},
+		{
+			name:     "custom type unmarshal func err",
+			elemType: reflect.TypeOf(testTextUnmarshaler{}),
+			config: &DecodeConfig{
+				TypeUnmarshalFuncs: map[reflect.Type]CustomizeDecodeFunc{
+					reflect.TypeOf(testTextUnmarshaler{}): func(req *protocol.Request, params param.Params, text string) (reflect.Value, error) {
+						return reflect.Value{}, errors.New("err")
+					},
+				},
+			},
+			expectError: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			req := &protocol.Request{}
+			params := param.Params{}
+			config := tt.config
+			if config == nil {
+				config = &DecodeConfig{}
+			}
+			val, err := stringToValue(tt.elemType, tt.text, req, params, config)
+			if tt.expectError {
+				assert.NotNil(t, err)
+				return
+			}
+			assert.Nil(t, err)
+			assert.DeepEqual(t, tt.expectValue, val.Interface())
+		})
+	}
+}
+
+func TestTryTextUnmarshaler(t *testing.T) {
+	tests := []struct {
+		name     string
+		value    interface{}
+		text     string
+		expected bool
+	}{
+		{
+			name:     "text unmarshaler",
+			value:    &testTextUnmarshaler{},
+			text:     "test text",
+			expected: true,
+		},
+		{
+			name:     "non text unmarshaler",
+			value:    &struct{}{},
+			text:     "test text",
+			expected: false,
+		},
+		{
+			name:     "nil value",
+			value:    nil,
+			text:     "test text",
+			expected: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var v reflect.Value
+			if tt.value != nil {
+				v = reflect.ValueOf(tt.value)
+			} else {
+				v = reflect.ValueOf(&tt.value).Elem()
+			}
+
+			result := tryTextUnmarshaler(v, tt.text)
+			assert.DeepEqual(t, tt.expected, result)
+
+			if tt.expected && tt.value != nil {
+				// Verify the value was actually set
+				unmarshaler := tt.value.(*testTextUnmarshaler)
+				assert.DeepEqual(t, tt.text, unmarshaler.Value)
+			}
+		})
+	}
+}
diff --git a/pkg/route/routes_timing_test.go b/pkg/route/routes_timing_test.go
index 4beb71d..d0f18a4 100644
--- a/pkg/route/routes_timing_test.go
+++ b/pkg/route/routes_timing_test.go
@@ -54,7 +54,7 @@ type Route struct {
 }
 
 func BenchmarkTree_FindStatic(b *testing.B) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	static := []*Route{
 		{"/"},
@@ -230,7 +230,7 @@ func BenchmarkTree_FindStatic(b *testing.B) {
 }
 
 func BenchmarkTree_FindGithub(b *testing.B) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	static := []*Route{
 		// OAuth Authorizations
@@ -508,7 +508,7 @@ func BenchmarkTree_FindGithub(b *testing.B) {
 }
 
 func BenchmarkTree_FindStaticTsr(b *testing.B) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/doc/foo/go_faq.html/",
@@ -530,7 +530,7 @@ func BenchmarkTree_FindStaticTsr(b *testing.B) {
 }
 
 func BenchmarkTree_FindParam(b *testing.B) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/hi/:key1/foo/:key2",
@@ -552,7 +552,7 @@ func BenchmarkTree_FindParam(b *testing.B) {
 }
 
 func BenchmarkTree_FindParamTsr(b *testing.B) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/hi/:key1/foo/:key2/",
@@ -574,7 +574,7 @@ func BenchmarkTree_FindParamTsr(b *testing.B) {
 }
 
 func BenchmarkTree_FindAny(b *testing.B) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/hi/*key1",
@@ -596,7 +596,7 @@ func BenchmarkTree_FindAny(b *testing.B) {
 }
 
 func BenchmarkTree_FindAnyFallback(b *testing.B) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/hi/a/b/c/d/e/*key1",
diff --git a/pkg/route/tree_test.go b/pkg/route/tree_test.go
index 296edfa..9da57ed 100644
--- a/pkg/route/tree_test.go
+++ b/pkg/route/tree_test.go
@@ -94,7 +94,7 @@ func TestCountParams(t *testing.T) {
 }
 
 func TestEmptyPath(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"",
@@ -113,7 +113,7 @@ func TestEmptyPath(t *testing.T) {
 }
 
 func TestTreeAddAndGet(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/hi",
@@ -150,7 +150,7 @@ func TestTreeAddAndGet(t *testing.T) {
 }
 
 func TestTreeWildcard(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/",
@@ -198,7 +198,7 @@ func TestTreeWildcard(t *testing.T) {
 }
 
 func TestUnescapeParameters(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/",
@@ -247,7 +247,7 @@ type testRoute struct {
 }
 
 func testRoutes(t *testing.T, routes []testRoute) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	for _, route := range routes {
 		recv := catchPanic(func() {
@@ -306,7 +306,7 @@ func TestTreeChildConflict(t *testing.T) {
 }
 
 func TestTreeDuplicatePath(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/",
@@ -342,7 +342,7 @@ func TestTreeDuplicatePath(t *testing.T) {
 }
 
 func TestEmptyWildcardName(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/user:",
@@ -372,7 +372,7 @@ func TestTreeCatchAllConflict(t *testing.T) {
 }
 
 func TestTreeCatchMaxParams(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 	route := "/cmd/*filepath"
 	tree.addRoute(route, fakeHandler(route))
 }
@@ -387,7 +387,7 @@ func TestTreeDoubleWildcard(t *testing.T) {
 	}
 
 	for _, route := range routes {
-		tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+		tree := &router{method: "GET", root: &node{}}
 		recv := catchPanic(func() {
 			tree.addRoute(route, nil)
 		})
@@ -399,7 +399,7 @@ func TestTreeDoubleWildcard(t *testing.T) {
 }
 
 func TestTreeTrailingSlashRedirect2(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/api/:version/seller/locales/get",
@@ -444,7 +444,7 @@ func TestTreeTrailingSlashRedirect2(t *testing.T) {
 }
 
 func TestTreeTrailingSlashRedirect(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	routes := [...]string{
 		"/hi",
@@ -543,7 +543,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
 }
 
 func TestTreeRootTrailingSlashRedirect(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	recv := catchPanic(func() {
 		tree.addRoute("/:test", fakeHandler("/:test"))
@@ -561,7 +561,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
 }
 
 func TestTreeFindCaseInsensitivePath(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 
 	longPath := "/l" + strings.Repeat("o", 128) + "ng"
 	lOngPath := "/l" + strings.Repeat("O", 128) + "ng/"
@@ -708,7 +708,7 @@ func TestTreeFindCaseInsensitivePath(t *testing.T) {
 }
 
 func TestTreeParamNotOptimize(t *testing.T) {
-	tree := &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree := &router{method: "GET", root: &node{}}
 	routes := [...]string{
 		"/:parama/start",
 		"/:paramb",
@@ -722,7 +722,7 @@ func TestTreeParamNotOptimize(t *testing.T) {
 	})
 
 	// other sequence
-	tree = &router{method: "GET", root: &node{}, hasTsrHandler: make(map[string]bool)}
+	tree = &router{method: "GET", root: &node{}}
 	routes = [...]string{
 		"/:paramb",
 		"/:parama/start",