Go
Go was built for HTTP. The stdlib net/http accepts any string as a method in http.NewRequest — no monkey-patching, no experimental flags, no extra dependencies. QUERY works out-of-the-box since Go 1.0. With the enhanced router in Go 1.22+, ServeMux accepts "QUERY /path" directly in patterns. Frameworks like Chi also support custom methods with a single registration call.
net/http Server (stdlib)
Section titled “net/http Server (stdlib)”The most idiomatic handler possible: Go 1.22+ ServeMux with "QUERY /path" pattern.
package main
import ( "encoding/json" "log" "net/http")
// SearchRequest represents the QUERY request body.type SearchRequest struct { Filter map[string]string `json:"filter"` Fields []string `json:"fields"` Limit int `json:"limit,omitempty"`}
// SearchResponse is the response payload.type SearchResponse struct { Results []map[string]any `json:"results"` Total int `json:"total"`}
func queryHandler(w http.ResponseWriter, r *http.Request) { var req SearchRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid json"}`, http.StatusBadRequest) return }
// Simulated filtered search results := []map[string]any{ {"id": 1, "name": "Mechanical Keyboard", "status": "active"}, {"id": 2, "name": "4K Monitor", "status": "active"}, }
resp := SearchResponse{Results: results, Total: len(results)}
w.Header().Set("Content-Type", "application/json") // QUERY is cacheable — set cache headers w.Header().Set("Cache-Control", "public, max-age=60") json.NewEncoder(w).Encode(resp)}
func main() { mux := http.NewServeMux() mux.HandleFunc("QUERY /products", queryHandler)
log.Println("server running on :8080") log.Fatal(http.ListenAndServe(":8080", mux))}Key points:
"QUERY /products"in the pattern — Go 1.22+ routes by method automatically.- No framework, no dependency. Pure stdlib.
Cache-Controlsignals the response is cacheable (QUERY semantics).
Client with http.NewRequest
Section titled “Client with http.NewRequest”The Go client already supports "QUERY" as a method — it’s just a string. No hacks.
package main
import ( "bytes" "encoding/json" "fmt" "io" "log" "net/http")
func main() { payload := map[string]any{ "filter": map[string]string{"status": "active", "category": "electronics"}, "fields": []string{"id", "name", "price"}, "limit": 10, }
body, err := json.Marshal(payload) if err != nil { log.Fatal(err) }
req, err := http.NewRequest("QUERY", "http://localhost:8080/products", bytes.NewReader(body)) if err != nil { log.Fatal(err) } req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req) if err != nil { log.Fatal(err) } defer resp.Body.Close()
data, _ := io.ReadAll(resp.Body) fmt.Printf("Status: %s\nBody: %s\n", resp.Status, data)}Why this works: http.NewRequest takes any string as the first argument. The HTTP/1.1 transport sends the literal method in the request line: QUERY /products HTTP/1.1.
Chi Router (custom method)
Section titled “Chi Router (custom method)”Chi requires chi.RegisterMethod in init() to recognize non-standard methods in routing.
package main
import ( "encoding/json" "log" "net/http"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware")
func init() { // Register QUERY as a recognized method in the router chi.RegisterMethod("QUERY")}
type SearchRequest struct { Filter map[string]string `json:"filter"` Fields []string `json:"fields"` Limit int `json:"limit,omitempty"`}
func main() { r := chi.NewRouter() r.Use(middleware.Logger) r.Use(middleware.Recoverer)
// MethodFunc registers a handler for a custom method r.MethodFunc("QUERY", "/products", func(w http.ResponseWriter, r *http.Request) { var req SearchRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, `{"error":"invalid json"}`, http.StatusBadRequest) return }
results := map[string]any{ "results": []map[string]any{ {"id": 1, "name": "Mechanical Keyboard", "price": 349.90}, }, "total": 1, "query": req, }
w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "public, max-age=60") json.NewEncoder(w).Encode(results) })
log.Println("chi server on :8080") log.Fatal(http.ListenAndServe(":8080", r))}Details:
chi.RegisterMethod("QUERY")ininit()— required before creating the router.r.MethodFunc("QUERY", "/products", handler)— registers the route.- All Chi middleware works normally (Logger, Recoverer, etc).
Testing with httptest
Section titled “Testing with httptest”Idiomatic integration test without spinning up a real server.
package main
import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing")
func TestQueryHandler(t *testing.T) { // Set up the handler mux := http.NewServeMux() mux.HandleFunc("QUERY /products", queryHandler)
// Create QUERY request with JSON body payload := map[string]any{ "filter": map[string]string{"status": "active"}, "fields": []string{"id", "name"}, } body, _ := json.Marshal(payload)
req := httptest.NewRequest("QUERY", "/products", bytes.NewReader(body)) req.Header.Set("Content-Type", "application/json")
// Record the response rec := httptest.NewRecorder() mux.ServeHTTP(rec, req)
// Check status if rec.Code != http.StatusOK { t.Fatalf("expected 200, got %d", rec.Code) }
// Check Content-Type ct := rec.Header().Get("Content-Type") if ct != "application/json" { t.Fatalf("expected application/json, got %s", ct) }
// Check Cache-Control (QUERY is cacheable) cc := rec.Header().Get("Cache-Control") if cc == "" { t.Fatal("expected Cache-Control header for QUERY method") }
// Decode response var resp SearchResponse if err := json.NewDecoder(rec.Body).Decode(&resp); err != nil { t.Fatalf("error decoding response: %v", err) }
if resp.Total == 0 { t.Fatal("expected results, got zero") }}
func TestQueryHandler_InvalidJSON(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("QUERY /products", queryHandler)
req := httptest.NewRequest("QUERY", "/products", bytes.NewReader([]byte("not json"))) req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder() mux.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest { t.Fatalf("expected 400 for invalid JSON, got %d", rec.Code) }}
func TestQueryHandler_WrongMethod(t *testing.T) { mux := http.NewServeMux() mux.HandleFunc("QUERY /products", queryHandler)
// GET should not hit the QUERY handler req := httptest.NewRequest("GET", "/products", nil) rec := httptest.NewRecorder() mux.ServeHTTP(rec, req)
if rec.Code == http.StatusOK { t.Fatal("GET should not return 200 on a QUERY route") }}httptest.NewRequest accepts any method — same principle as http.NewRequest. No magic.
Testing with curl
Section titled “Testing with curl”curl supports any method via -X. Here are the most useful scenarios:
# Basic QUERY request with JSON bodycurl -X QUERY http://localhost:8080/products \ -H "Content-Type: application/json" \ -d '{"filter":{"status":"active"},"fields":["id","name"],"limit":10}'
curl -X QUERY http://localhost:8080/products \ -H "Content-Type: application/json" \ -d '{"filter":{"category":"electronics"}}' \ -v
echo '{"filter":{"status":"active","price_min":100},"fields":["id","name","price"]}' > query.jsoncurl -X QUERY http://localhost:8080/products \ -H "Content-Type: application/json" \ -d @query.json
curl -X QUERY http://localhost:8080/products \ -H "Content-Type: application/json" \ -d '{"filter":{"status":"active"}}' \ -s -o /dev/null -D -Expected verbose output:
> QUERY /products HTTP/1.1> Host: localhost:8080> Content-Type: application/json> Content-Length: 65>< HTTP/1.1 200 OK< Content-Type: application/json< Cache-Control: public, max-age=60Why Go is ideal for QUERY
Section titled “Why Go is ideal for QUERY”| Aspect | How Go handles it |
|---|---|
| Custom method | http.NewRequest("QUERY", ...) — just a string |
| Routing | ServeMux Go 1.22+ accepts "QUERY /path" |
| Body in safe method | Transport sends body normally |
| Cache semantics | Standard headers, no hacks |
| Testing | httptest.NewRequest("QUERY", ...) directly |
| Frameworks | Chi: RegisterMethod + MethodFunc |
No reflection, no build tags, no flags. The net/http design has supported arbitrary methods since day one.