Skip to content

Commit abbb92e

Browse files
thdxgclaude
authored andcommitted
feat: add support for HTTP QUERY method (#3036)
Add first-class support for the QUERY HTTP method (RFC 10008) so it can be registered with dedicated helpers instead of the generic `Add`. QUERY is not (yet) part of the `net/http` standard library, so it is defined in Echo the same way the existing non-standard PROPFIND and REPORT methods are handled: - add the `QUERY` method constant - add `Echo.QUERY` and `Group.QUERY` route helpers - give QUERY a dedicated field in `routeMethods` and wire it into `set`, `find`, `updateAllowHeader` and `isHandler` - bind URL query parameters for QUERY (like GET/DELETE/HEAD), since QUERY is semantically a GET carrying its query in the body Closes #3036 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 11aa371 commit abbb92e

8 files changed

Lines changed: 68 additions & 2 deletions

File tree

bind.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,11 +125,11 @@ func (b *DefaultBinder) Bind(c *Context, target any) error {
125125
if err := BindPathValues(c, target); err != nil {
126126
return err
127127
}
128-
// Only bind query parameters for GET/DELETE/HEAD to avoid unexpected behavior with destination struct binding from body.
128+
// Only bind query parameters for GET/DELETE/HEAD/QUERY to avoid unexpected behavior with destination struct binding from body.
129129
// For example a request URL `&id=1&lang=en` with body `{"id":100,"lang":"de"}` would lead to precedence issues.
130130
// The HTTP method check restores pre-v4.1.11 behavior to avoid these problems (see issue #1670)
131131
method := c.Request().Method
132-
if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead {
132+
if method == http.MethodGet || method == http.MethodDelete || method == http.MethodHead || method == QUERY {
133133
if err := BindQueryParams(c, target); err != nil {
134134
return err
135135
}

bind_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,13 @@ func TestDefaultBinder_BindToStructFromMixedSources(t *testing.T) {
814814
givenContent: strings.NewReader(`{"id": 1, "node": "zzz"}`),
815815
expect: &Opts{ID: 1, Node: "zzz"}, // for DELETE body is bound after query params
816816
},
817+
{
818+
name: "ok, QUERY bind to struct with: path param + query param + body",
819+
givenMethod: QUERY,
820+
givenURL: "/api/real_node/endpoint?node=xxx",
821+
givenContent: strings.NewReader(`{"id": 1}`),
822+
expect: &Opts{ID: 1, Node: "xxx"}, // for QUERY query overwrites previous path value (like GET)
823+
},
817824
{
818825
name: "ok, POST bind to struct with: path param + body",
819826
givenMethod: http.MethodPost,

echo.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,9 @@ const (
170170
PROPFIND = "PROPFIND"
171171
// REPORT Method can be used to get information about a resource, see rfc 3253
172172
REPORT = "REPORT"
173+
// QUERY Method is a safe, idempotent request method carrying request content (a query) in its body, see rfc 10008.
174+
// It is not (yet) part of the `net/http` standard library, so Echo defines it here.
175+
QUERY = "QUERY"
173176
// RouteNotFound is special method type for routes handling "route not found" (404) cases
174177
RouteNotFound = "echo_route_not_found"
175178
// RouteAny is special method type that matches any HTTP method in request. Any has lower
@@ -539,6 +542,12 @@ func (e *Echo) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {
539542
return e.Add(http.MethodPut, path, h, m...)
540543
}
541544

545+
// QUERY registers a new QUERY route for a path with matching handler in the
546+
// router with optional route-level middleware. Panics on error.
547+
func (e *Echo) QUERY(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {
548+
return e.Add(QUERY, path, h, m...)
549+
}
550+
542551
// TRACE registers a new TRACE route for a path with matching handler in the
543552
// router with optional route-level middleware. Panics on error.
544553
func (e *Echo) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {

echo_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,23 @@ func TestEchoTrace(t *testing.T) {
908908
assert.Equal(t, "OK", body)
909909
}
910910

911+
func TestEchoQuery(t *testing.T) {
912+
e := New()
913+
914+
ri := e.QUERY("/", func(c *Context) error {
915+
return c.String(http.StatusTeapot, "OK")
916+
})
917+
918+
assert.Equal(t, QUERY, ri.Method)
919+
assert.Equal(t, "/", ri.Path)
920+
assert.Equal(t, QUERY+":/", ri.Name)
921+
assert.Nil(t, ri.Parameters)
922+
923+
status, body := request(QUERY, "/", e)
924+
assert.Equal(t, http.StatusTeapot, status)
925+
assert.Equal(t, "OK", body)
926+
}
927+
911928
func TestEcho_Any(t *testing.T) {
912929
e := New()
913930

group.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ func (g *Group) PUT(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {
6363
return g.Add(http.MethodPut, path, h, m...)
6464
}
6565

66+
// QUERY implements `Echo#QUERY()` for sub-routes within the Group. Panics on error.
67+
func (g *Group) QUERY(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {
68+
return g.Add(QUERY, path, h, m...)
69+
}
70+
6671
// TRACE implements `Echo#TRACE()` for sub-routes within the Group. Panics on error.
6772
func (g *Group) TRACE(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo {
6873
return g.Add(http.MethodTrace, path, h, m...)

group_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,24 @@ func TestGroup_TRACE(t *testing.T) {
306306
assert.Equal(t, `OK`, body)
307307
}
308308

309+
func TestGroup_QUERY(t *testing.T) {
310+
e := New()
311+
312+
users := e.Group("/users")
313+
ri := users.QUERY("/activate", func(c *Context) error {
314+
return c.String(http.StatusTeapot, "OK")
315+
})
316+
317+
assert.Equal(t, QUERY, ri.Method)
318+
assert.Equal(t, "/users/activate", ri.Path)
319+
assert.Equal(t, QUERY+":/users/activate", ri.Name)
320+
assert.Nil(t, ri.Parameters)
321+
322+
status, body := request(QUERY, "/users/activate", e)
323+
assert.Equal(t, http.StatusTeapot, status)
324+
assert.Equal(t, `OK`, body)
325+
}
326+
309327
func TestGroup_RouteNotFound(t *testing.T) {
310328
var testCases = []struct {
311329
expectRoute any

router.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ type routeMethods struct {
161161
put *routeMethod
162162
trace *routeMethod
163163
report *routeMethod
164+
query *routeMethod
164165
any *routeMethod
165166
anyOther map[string]*routeMethod
166167

@@ -196,6 +197,8 @@ func (m *routeMethods) set(method string, r *routeMethod) {
196197
m.trace = r
197198
case REPORT:
198199
m.report = r
200+
case QUERY:
201+
m.query = r
199202
case RouteAny:
200203
m.any = r
201204
case RouteNotFound:
@@ -239,6 +242,8 @@ func (m *routeMethods) find(method string, fallbackToAny bool) *routeMethod {
239242
r = m.trace
240243
case REPORT:
241244
r = m.report
245+
case QUERY:
246+
r = m.query
242247
case RouteAny:
243248
r = m.any
244249
case RouteNotFound:
@@ -295,6 +300,9 @@ func (m *routeMethods) updateAllowHeader() {
295300
if hasAnyMethod || m.report != nil {
296301
buf.WriteString(", REPORT")
297302
}
303+
if hasAnyMethod || m.query != nil {
304+
buf.WriteString(", QUERY")
305+
}
298306
for method := range m.anyOther { // for simplicity, we use map and therefore order is not deterministic here
299307
buf.WriteString(", ")
300308
buf.WriteString(method)
@@ -314,6 +322,7 @@ func (m *routeMethods) isHandler() bool {
314322
m.propfind != nil ||
315323
m.trace != nil ||
316324
m.report != nil ||
325+
m.query != nil ||
317326
m.any != nil ||
318327
len(m.anyOther) != 0
319328
// RouteNotFound/404 is not considered as a handler

router_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,7 @@ func TestRouter_addAndMatchAllSupportedMethods(t *testing.T) {
761761
{name: "ok, PUT", whenMethod: http.MethodPut},
762762
{name: "ok, TRACE", whenMethod: http.MethodTrace},
763763
{name: "ok, REPORT", whenMethod: REPORT},
764+
{name: "ok, QUERY", whenMethod: QUERY},
764765
{name: "ok, NON_TRADITIONAL_METHOD", whenMethod: "NON_TRADITIONAL_METHOD"},
765766
{
766767
name: "ok, NOT_EXISTING_METHOD",

0 commit comments

Comments
 (0)