chore(deps): bump go-pkgz/rest to v1.22.0, drop local CORS Vary workaround
v1.22.0 includes the preflight Vary fix (https://github.com/go-pkgz/rest/pull/44): rest.CORS now adds Vary: Access-Control-Request-Method and Access-Control-Request-Headers on preflight itself, making the local wrapper that added them redundant. corsMiddleware now returns rest.CORS directly; TestCorsMiddleware still asserts those preflight Vary headers, now supplied upstream. Also tidies the _example/memory_store module for the new version.
This commit is contained in:
committed by
Umputun
parent
3fc5d6b970
commit
c48254a994
@@ -18,7 +18,7 @@ require (
|
||||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dlclark/regexp2/v2 v2.2.2 // indirect
|
||||
github.com/go-pkgz/rest v1.21.0 // indirect
|
||||
github.com/go-pkgz/rest v1.22.0 // indirect
|
||||
github.com/go-pkgz/routegroup v1.6.0 // indirect
|
||||
github.com/gorilla/css v1.0.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
|
||||
@@ -21,8 +21,8 @@ github.com/go-pkgz/jrpc v0.4.0 h1:oD7xiGrzDkndkuCjeHGugQXxbggLSV7O1QmHhoc5pYY=
|
||||
github.com/go-pkgz/jrpc v0.4.0/go.mod h1:JFoY3bRjRyx4M3CbEVDFQStMB1m2gmQ7OjqFK7q3kOo=
|
||||
github.com/go-pkgz/lgr v0.12.3 h1:QDug7kRkEsuQtruT9fNF5PVT2kZUqCDPc4GmsgS3fP8=
|
||||
github.com/go-pkgz/lgr v0.12.3/go.mod h1:lpCDgVvCIxBHZp8+sGCj9MPctIzKZyZ3QdE19ddqd54=
|
||||
github.com/go-pkgz/rest v1.21.0 h1:Y/C4d/TpclJJDxqnH1RAcS6Hmox0RIReAlkwMcUWXK4=
|
||||
github.com/go-pkgz/rest v1.21.0/go.mod h1:+AHzjHazq7Z3Tk/kRWOhbbAz/YZlUV40feC1Hf4NtbE=
|
||||
github.com/go-pkgz/rest v1.22.0 h1:d3XFKlmAGBiU9MQER9/n46iXpyUr8IQUtfjU8JlqkkY=
|
||||
github.com/go-pkgz/rest v1.22.0/go.mod h1:+AHzjHazq7Z3Tk/kRWOhbbAz/YZlUV40feC1Hf4NtbE=
|
||||
github.com/go-pkgz/routegroup v1.6.0 h1:44XHZgF6JIIldRlv+zjg6SygULASmjifnfIQjwCT0e4=
|
||||
github.com/go-pkgz/routegroup v1.6.0/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg=
|
||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
// Access-Control-Allow-Origin (rather than a literal "*"), which browsers require
|
||||
// for credentialed cross-origin requests.
|
||||
func corsMiddleware() func(http.Handler) http.Handler {
|
||||
cors := R.CORS(
|
||||
return R.CORS(
|
||||
R.CorsAllowedOrigins("*"),
|
||||
R.CorsAllowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS"),
|
||||
R.CorsAllowedHeaders("Accept", "Authorization", "Content-Type", "X-XSRF-Token", "X-JWT"),
|
||||
@@ -31,19 +31,6 @@ func corsMiddleware() func(http.Handler) http.Handler {
|
||||
R.CorsAllowCredentials(true),
|
||||
R.CorsMaxAge(300),
|
||||
)
|
||||
return func(next http.Handler) http.Handler {
|
||||
corsHandler := cors(next)
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// rest.CORS only sets "Vary: Origin"; for preflight also vary on the requested
|
||||
// method and headers so caches/proxies don't reuse one preflight response across
|
||||
// different requests (preserving the prior go-chi/cors behavior).
|
||||
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
|
||||
w.Header().Add("Vary", "Access-Control-Request-Method")
|
||||
w.Header().Add("Vary", "Access-Control-Request-Headers")
|
||||
}
|
||||
corsHandler.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// timeout returns a middleware matching chi's middleware.Timeout: it sets a
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ require (
|
||||
github.com/go-pkgz/lgr v0.12.3
|
||||
github.com/go-pkgz/notify v1.3.0
|
||||
github.com/go-pkgz/repeater/v2 v2.2.0
|
||||
github.com/go-pkgz/rest v1.21.0
|
||||
github.com/go-pkgz/rest v1.22.0
|
||||
github.com/go-pkgz/routegroup v1.6.0
|
||||
github.com/go-pkgz/syncs v1.3.2
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
|
||||
+2
-2
@@ -60,8 +60,8 @@ github.com/go-pkgz/repeater v1.2.0 h1:oJFvjyKdTDd5RCzpzxlzYIZFFj6Zfl17rE1aUfu6Uj
|
||||
github.com/go-pkgz/repeater v1.2.0/go.mod h1:vypP6xamA53MFmafnGUucqOmALKk36xgKu2hSG73LHM=
|
||||
github.com/go-pkgz/repeater/v2 v2.2.0 h1:8nZR/NaknmLfx2YMHbr78u9OL4Xj+8+romm9dz4FpMg=
|
||||
github.com/go-pkgz/repeater/v2 v2.2.0/go.mod h1:RgX5vUbLKq7PV82QUDP5pFbQS1os4Z+U9XzKymK23A8=
|
||||
github.com/go-pkgz/rest v1.21.0 h1:Y/C4d/TpclJJDxqnH1RAcS6Hmox0RIReAlkwMcUWXK4=
|
||||
github.com/go-pkgz/rest v1.21.0/go.mod h1:+AHzjHazq7Z3Tk/kRWOhbbAz/YZlUV40feC1Hf4NtbE=
|
||||
github.com/go-pkgz/rest v1.22.0 h1:d3XFKlmAGBiU9MQER9/n46iXpyUr8IQUtfjU8JlqkkY=
|
||||
github.com/go-pkgz/rest v1.22.0/go.mod h1:+AHzjHazq7Z3Tk/kRWOhbbAz/YZlUV40feC1Hf4NtbE=
|
||||
github.com/go-pkgz/routegroup v1.6.0 h1:44XHZgF6JIIldRlv+zjg6SygULASmjifnfIQjwCT0e4=
|
||||
github.com/go-pkgz/routegroup v1.6.0/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg=
|
||||
github.com/go-pkgz/syncs v1.3.2 h1:gmioASlJNy3gNosPlgvWOM2QP0Hdjzn2u+/sUShgd8E=
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## What this is
|
||||
|
||||
`github.com/go-pkgz/rest` is a library of HTTP middlewares and small helpers for REST services. It is not an application — there is no `main`, no server, no CLI. It is consumed by other projects (remark42, etc.). Keep the public API stable and minimal.
|
||||
|
||||
Dependencies are deliberately tiny: only `stretchr/testify` (tests) and `golang.org/x/crypto` (Argon2/bcrypt for BasicAuth). Do not add a routing framework or logging library dependency — middlewares are plain stdlib and must stay router-agnostic.
|
||||
|
||||
## Commands
|
||||
|
||||
- Test everything: `go test ./...`
|
||||
- Single test: `go test -run TestName ./...` (e.g. `go test -run TestPing ./...`)
|
||||
- Race + coverage (mirrors CI): `TZ="America/Chicago" go test -timeout=60s -race -covermode=atomic ./...`
|
||||
- Lint (run from repo root): `golangci-lint run --max-issues-per-linter=0 --max-same-issues=0`
|
||||
|
||||
Time-parsing tests (`ParseFromTo`) are timezone-sensitive; CI runs with `TZ=America/Chicago`. Set it locally if a from/to test behaves oddly.
|
||||
|
||||
## Architecture
|
||||
|
||||
Three packages:
|
||||
- **`rest`** (root) — all middlewares plus JSON/error/file-server helpers.
|
||||
- **`logger`** — request-logging middleware, split out so it can be wired to any backend via the `logger.Backend` interface (`Logf(format, args...)`). Configured with functional options (`logger.New(logger.Prefix(...), logger.WithBody, ...)`).
|
||||
- **`realip`** — `realip.Get(r)` extracts the client IP from proxy headers. Used by both `rest.RealIP` and the `logger` package. Only public IPs are accepted from headers.
|
||||
|
||||
### Middleware conventions
|
||||
|
||||
Every middleware is a `func(http.Handler) http.Handler` (or a `func(...) func(http.Handler) http.Handler` when it takes config). This is the chi/stdlib-compatible shape — match it for any new middleware. Some middlewares short-circuit the chain (`Ping`, `Health`, metrics) by writing a response and returning without calling the next handler.
|
||||
|
||||
Configurable middlewares use the **functional-options pattern**, not option structs:
|
||||
- `CORS(CorsAllowedOrigins(...), CorsAllowCredentials(true), ...)` — options named `CorsXxx`.
|
||||
- `Secure(SecFrameOptions(...), SecHSTS(...), ...)` — options named `SecXxx`, plus `SecAllHeaders()` convenience.
|
||||
- `logger.New(logger.Prefix(...), ...)` — options in `logger/options.go`.
|
||||
|
||||
When adding an option to one of these, follow the existing prefix/naming and keep defaults sensible so calling the constructor with no options is safe.
|
||||
|
||||
### CSRF build-tag split (important)
|
||||
|
||||
CSRF protection has **two implementations behind one identical public API**, selected by Go version:
|
||||
- `csrf_go125.go` (`//go:build go1.25`) — thin wrapper over stdlib `http.CrossOriginProtection`.
|
||||
- `csrf.go` (`//go:build !go1.25`) — self-contained equivalent for older Go.
|
||||
|
||||
`NewCrossOriginProtection`, `AddTrustedOrigin`, `AddBypassPattern`, `SetDenyHandler`, `Check`, `Handler` must exist with the **same signatures and behavior in both files**. When you change the CSRF API or behavior, edit both files and keep `csrf_test.go` passing under both build tags. go.mod targets go 1.24, so by default the `!go1.25` path compiles unless building with a 1.25 toolchain.
|
||||
|
||||
### Helpers
|
||||
|
||||
`rest.go` holds the JSON render/encode/decode helpers (`RenderJSON`, `EncodeJSON`/`DecodeJSON` generics, `RenderJSONWithHTML`) and `ParseFromTo`. `httperrors.go` has `SendErrorJSON`/`NewErrorLogger`. `file_server.go` has `FileServer` (directory listing disabled by design). `benchmarks.go` keeps an in-memory ring of up to 900 per-second data points (15 min) queried via `Stats(duration)`.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **One test file per source file**: `foo.go` → `foo_test.go` only. Table-driven with testify. Note `depricattion.go`/`depricattion_test.go` is misspelled but is the real filename — don't "fix" it without intent, it's the established path.
|
||||
- After changing or adding a middleware/helper, update `README.md` — it documents every middleware and helper and is the primary user-facing doc.
|
||||
- Lint config (`.golangci.yml`) is strict (`govet enable-all`, revive, gocritic with performance/style/experimental). `modernize` is enabled — prefer `any` over `interface{}`, `slices`/`maps` stdlib, etc.
|
||||
+9
@@ -182,6 +182,14 @@ RealIP is a middleware that sets a http.Request's RemoteAddr to the results of p
|
||||
|
||||
Only public IPs are accepted from headers; private/loopback/link-local IPs are skipped. This makes the middleware compatible with CDN setups like Cloudflare where the leftmost IP in `X-Forwarded-For` is the actual client.
|
||||
|
||||
### Timeout middleware
|
||||
|
||||
Timeout bounds a request to the given duration and responds with `StatusGatewayTimeout` (504) at the deadline if the handler has not finished — even for a handler that does not observe the context. The handler runs with a context deadline and its output is buffered; on success the buffered response is written through unchanged, and if the deadline fires first the buffered output is discarded, a 504 is sent, and further writes by the still-running handler return `http.ErrHandlerTimeout`. Because the response is buffered, `http.Flusher` and `http.Hijacker` are not available under `Timeout` (as with `net/http.TimeoutHandler`), so it is not suitable for streaming or connection-hijacking handlers. A non-positive duration disables the middleware (the handler is called directly).
|
||||
|
||||
```go
|
||||
router.Use(rest.Timeout(5 * time.Second))
|
||||
```
|
||||
|
||||
### CORS middleware
|
||||
|
||||
Handles Cross-Origin Resource Sharing, allowing controlled access from different origins.
|
||||
@@ -213,6 +221,7 @@ Features:
|
||||
- Origin validation with case-insensitive matching
|
||||
- Credentials support (reflects origin instead of `*`)
|
||||
- Configurable cache duration for preflight results
|
||||
- Cache-correct `Vary` headers (adds `Access-Control-Request-Method` and `Access-Control-Request-Headers` on preflight)
|
||||
|
||||
Available options:
|
||||
- `CorsAllowedOrigins(origins...)` - allowed origins (default: `*`)
|
||||
|
||||
+3
@@ -158,6 +158,9 @@ func CORS(opts ...CorsOpt) func(http.Handler) http.Handler {
|
||||
// handle preflight request
|
||||
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
|
||||
// preflight request
|
||||
// the response varies by the requested method and headers, so caches must key on them
|
||||
w.Header().Add("Vary", "Access-Control-Request-Method")
|
||||
w.Header().Add("Vary", "Access-Control-Request-Headers")
|
||||
w.Header().Set("Access-Control-Allow-Methods", methodsStr)
|
||||
w.Header().Set("Access-Control-Allow-Headers", headersStr)
|
||||
|
||||
|
||||
+2
-2
@@ -146,8 +146,8 @@ func (l *Middleware) formatDefault(r *http.Request, p *logParts) string {
|
||||
_, _ = bld.WriteString(" ")
|
||||
}
|
||||
|
||||
_, _ = bld.WriteString(fmt.Sprintf("%s - %s - %s - %s - %d (%d) - %v",
|
||||
p.method, p.rawURL, p.host, p.remoteIP, p.statusCode, p.respSize, p.duration))
|
||||
_, _ = fmt.Fprintf(&bld, "%s - %s - %s - %s - %d (%d) - %v",
|
||||
p.method, p.rawURL, p.host, p.remoteIP, p.statusCode, p.respSize, p.duration)
|
||||
|
||||
if p.user != "" {
|
||||
_, _ = bld.WriteString(" - ")
|
||||
|
||||
+3
-2
@@ -5,6 +5,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-pkgz/rest/logger"
|
||||
@@ -13,8 +14,8 @@ import (
|
||||
|
||||
// Wrap converts a list of middlewares to nested calls (in reverse order)
|
||||
func Wrap(handler http.Handler, mws ...func(http.Handler) http.Handler) http.Handler {
|
||||
for i := len(mws) - 1; i >= 0; i-- {
|
||||
handler = mws[i](handler)
|
||||
for _, mw := range slices.Backward(mws) {
|
||||
handler = mw(handler)
|
||||
}
|
||||
return handler
|
||||
}
|
||||
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package rest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"maps"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Timeout is a middleware that enforces a maximum duration for handling a request.
|
||||
// It runs the next handler with a context deadline and, if the handler has not finished
|
||||
// by the time the deadline is reached, responds with StatusGatewayTimeout (504) at the
|
||||
// deadline — regardless of whether the handler observes the context.
|
||||
//
|
||||
// The handler's output is buffered until it completes: on success the buffered response
|
||||
// (status, headers and body) is written through unchanged; if the deadline fires first,
|
||||
// the buffered output is discarded, a 504 is sent, and any further writes by the still
|
||||
// running handler return http.ErrHandlerTimeout. If the parent request context is
|
||||
// canceled (rather than the deadline being exceeded) the handler is stopped without a
|
||||
// 504, since the request is being abandoned, and its later writes return the context's
|
||||
// error (e.g. context.Canceled).
|
||||
//
|
||||
// Because the response is buffered, the wrapped ResponseWriter does not support
|
||||
// http.Flusher or http.Hijacker (matching net/http.TimeoutHandler); streaming and
|
||||
// connection hijacking are not available under Timeout.
|
||||
//
|
||||
// A non-positive timeout disables the middleware: the handler is called directly, with
|
||||
// no deadline, buffering or 504.
|
||||
func Timeout(timeout time.Duration) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
if timeout <= 0 {
|
||||
return next
|
||||
}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), timeout)
|
||||
defer cancel()
|
||||
r = r.WithContext(ctx)
|
||||
|
||||
done := make(chan struct{})
|
||||
panicChan := make(chan any, 1)
|
||||
tw := &timeoutWriter{w: w, h: make(http.Header)}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
if p := recover(); p != nil {
|
||||
panicChan <- p
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(tw, r)
|
||||
close(done)
|
||||
}()
|
||||
|
||||
select {
|
||||
case p := <-panicChan:
|
||||
panic(p)
|
||||
case <-done:
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
maps.Copy(w.Header(), tw.h)
|
||||
code := http.StatusOK
|
||||
if tw.wroteHeader {
|
||||
code = tw.code
|
||||
}
|
||||
w.WriteHeader(code)
|
||||
_, _ = w.Write(tw.wbuf.Bytes())
|
||||
case <-ctx.Done():
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
// discard the buffer and stop further handler writes, recording the cause so a
|
||||
// late write returns the real error. Only the deadline yields a 504; a canceled
|
||||
// parent means the request is being abandoned, so there is nobody to send it to.
|
||||
switch err := ctx.Err(); err {
|
||||
case context.DeadlineExceeded:
|
||||
tw.err = http.ErrHandlerTimeout
|
||||
w.WriteHeader(http.StatusGatewayTimeout)
|
||||
default:
|
||||
tw.err = err
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// timeoutWriter buffers a handler's response so the Timeout middleware can either flush
|
||||
// it on success or discard it and send a 504 once the deadline is reached. All fields
|
||||
// after mu are guarded by mu, which is also held by the middleware while it drains the
|
||||
// buffer, so it is safe against a handler goroutine that keeps writing after the timeout.
|
||||
type timeoutWriter struct {
|
||||
w http.ResponseWriter
|
||||
h http.Header
|
||||
|
||||
mu sync.Mutex
|
||||
wbuf bytes.Buffer
|
||||
code int
|
||||
wroteHeader bool
|
||||
err error // set once the response is timed out or canceled; returned by Write
|
||||
}
|
||||
|
||||
// Header implements http.ResponseWriter and returns the buffered header map.
|
||||
func (tw *timeoutWriter) Header() http.Header { return tw.h }
|
||||
|
||||
// Write implements http.ResponseWriter, buffering the response body. Once the response has
|
||||
// timed out or been canceled it buffers nothing and returns the recorded error
|
||||
// (http.ErrHandlerTimeout on deadline, or the context error on cancellation).
|
||||
func (tw *timeoutWriter) Write(p []byte) (int, error) {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
if tw.err != nil {
|
||||
return 0, tw.err
|
||||
}
|
||||
if !tw.wroteHeader {
|
||||
tw.setHeaderLocked(http.StatusOK)
|
||||
}
|
||||
return tw.wbuf.Write(p)
|
||||
}
|
||||
|
||||
// WriteHeader implements http.ResponseWriter, recording the status for the buffered
|
||||
// response. It is a no-op after the timeout has fired or after the first call.
|
||||
func (tw *timeoutWriter) WriteHeader(code int) {
|
||||
tw.mu.Lock()
|
||||
defer tw.mu.Unlock()
|
||||
tw.setHeaderLocked(code)
|
||||
}
|
||||
|
||||
func (tw *timeoutWriter) setHeaderLocked(code int) {
|
||||
if tw.err != nil || tw.wroteHeader {
|
||||
return
|
||||
}
|
||||
tw.wroteHeader = true
|
||||
tw.code = code
|
||||
}
|
||||
Vendored
+1
-1
@@ -82,7 +82,7 @@ github.com/go-pkgz/repeater/strategy
|
||||
# github.com/go-pkgz/repeater/v2 v2.2.0
|
||||
## explicit; go 1.23
|
||||
github.com/go-pkgz/repeater/v2
|
||||
# github.com/go-pkgz/rest v1.21.0
|
||||
# github.com/go-pkgz/rest v1.22.0
|
||||
## explicit; go 1.24.0
|
||||
github.com/go-pkgz/rest
|
||||
github.com/go-pkgz/rest/logger
|
||||
|
||||
Reference in New Issue
Block a user