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:
Dmitry Verkhoturov
2026-07-03 15:40:10 -05:00
committed by Umputun
parent 3fc5d6b970
commit c48254a994
12 changed files with 212 additions and 25 deletions
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+1 -14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+1 -1
View File
@@ -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