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.
134 lines
4.3 KiB
Go
134 lines
4.3 KiB
Go
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
|
|
}
|