Skip to content

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.


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-Control signals the response is cacheable (QUERY semantics).

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 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") in init() — required before creating the router.
  • r.MethodFunc("QUERY", "/products", handler) — registers the route.
  • All Chi middleware works normally (Logger, Recoverer, etc).

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.


curl supports any method via -X. Here are the most useful scenarios:

Terminal window
# Basic QUERY request with JSON body
curl -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.json
curl -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=60

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.