Skip to content

feat: add Timeout middleware#45

Open
paskal wants to merge 1 commit into
masterfrom
timeout-middleware
Open

feat: add Timeout middleware#45
paskal wants to merge 1 commit into
masterfrom
timeout-middleware

Conversation

@paskal

@paskal paskal commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

go-pkgz/rest has Throttle, RealIP, NoCache, CORS, Recoverer and friends, but no Timeout middleware, so consumers hand-roll one. This adds Timeout.

func Timeout(timeout time.Duration) func(http.Handler) http.Handler

It enforces a maximum request duration, modeled on net/http.TimeoutHandler: the next handler runs in a goroutine with a context deadline, and if it has not finished by the deadline the middleware responds with StatusGatewayTimeout (504) at the deadline — even for a handler that ignores the context.

The handler's output is buffered and written through on success; if the deadline fires first the buffered output is discarded, a 504 is sent, and further handler writes return http.ErrHandlerTimeout. A canceled parent context stops the handler without sending a 504, and panics propagate. Because the response is buffered, http.Flusher and http.Hijacker are not available under Timeout (same trade-off as net/http.TimeoutHandler).

Tests

TestTimeout (subtests, millisecond deadlines, -race clean, 100% coverage of timeout.go) covers: fast pass-through with status/headers/body; default 200; the context carrying the deadline; a context-ignoring handler timed out at the deadline (output discarded); a context-respecting handler timing out; partial output discarded on timeout; post-timeout writes returning http.ErrHandlerTimeout; duplicate/after-timeout WriteHeader no-ops; panic propagation; and canceled-parent-context stopping the handler without a 504.

Found while migrating umputun/remark42 off go-chi onto go-pkgz/rest.

@paskal paskal requested a review from umputun as a code owner June 30, 2026 22:46
@paskal paskal force-pushed the timeout-middleware branch from 9ab6040 to 7d10733 Compare June 30, 2026 23:08
@paskal

paskal commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

Addressed a review finding: the deferred 504 could become a superfluous WriteHeader if the handler had already committed a response before the deadline tripped (and there was no regression test for that path). The middleware now wraps the ResponseWriter to track whether the response was committed and skips the 504 in that case. Added a test using a recording writer (which, unlike httptest.ResponseRecorder, captures every WriteHeader call) — it sees [201, 504] without the fix and [201] with it. go test -race and gofmt/vet clean.

Note: the red build check is a pre-existing repo-wide golangci-lint issue (middleware.go:16 slicesbackward, logger/logger.go:149 QF1012) surfaced by golangci-lint latest advancing to v2.12.2 — unrelated to this PR; nothing in this PR's files is flagged.

@coveralls

coveralls commented Jun 30, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 28485193325

Coverage increased (+0.2%) to 96.202%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 65 of 65 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 1290
Covered Lines: 1241
Line Coverage: 96.2%
Coverage Strength: 51.41 hits per line

💛 - Coveralls

@paskal paskal marked this pull request as draft June 30, 2026 23:30
@paskal paskal force-pushed the timeout-middleware branch from 51eb7fd to 99a7300 Compare June 30, 2026 23:34
@paskal

paskal commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto master now that #44 is merged (the duplicate golangci-lint commit dropped automatically — patch already upstream), so this is now a single clean commit. Also fixed the red coverage/coveralls: the wrapper's Flush/Hijack forwarding was untested — added tests so timeout.go is 100% covered (Timeout/WriteHeader/Write/Flush/Hijack). Tightened the README Timeout description to note the 504 is only sent if the deadline trips before the handler writes, and an already-started response is left untouched. go test -race, gofmt, vet and golangci-lint all clean.

@paskal paskal requested a review from Copilot June 30, 2026 23:38
@paskal paskal marked this pull request as ready for review June 30, 2026 23:39

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Timeout middleware to the rest package to align with the existing middleware set (e.g., Throttle, RealIP, NoCache, etc.) by canceling the request context after a configured duration and (when possible) returning HTTP 504 if the deadline is exceeded before a response is written.

Changes:

  • Introduces Timeout(timeout time.Duration) middleware that wraps the request context with context.WithTimeout and conditionally writes StatusGatewayTimeout (504).
  • Adds a timeoutWriter response-writer wrapper and a comprehensive TestTimeout suite covering core behaviors and interface forwarding.
  • Documents the new middleware usage and semantics in README.md.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
timeout.go Implements Timeout middleware and timeoutWriter wrapper to track whether a response has been committed.
timeout_test.go Adds tests for timeout behavior, committed-response handling, and forwarding of Flusher/Hijacker.
README.md Documents Timeout middleware semantics and usage example.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread timeout.go Outdated
Comment thread timeout.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

Comment thread timeout.go Outdated
Comment thread timeout.go Outdated
Comment thread timeout_test.go Outdated
@paskal

paskal commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Reworked the middleware from a cooperative design (504 only after a context-respecting handler returns, chi-style) to an enforcing one modeled on net/http.TimeoutHandler, per maintainer decision: the handler runs in a goroutine with a context deadline, and a 504 is sent at the deadline even if the handler ignores the context. Output is buffered and flushed on success; on timeout it's discarded, a 504 is sent, and further handler writes return http.ErrHandlerTimeout. A canceled parent context stops the handler without a 504; panics propagate.

This addresses the earlier review points (the buffered/enforcing model removes the superfluous-504, Flush/Hijack-commit, Unwrap and 1xx/101 concerns entirely). Trade-off, documented: buffering means http.Flusher/http.Hijacker aren't supported (same as net/http.TimeoutHandler).

Verified: 100% coverage of timeout.go, go test -race clean (x3), go vet + golangci-lint clean. A focused concurrency review found no data races (Header map copied only after the handler goroutine finishes; the real ResponseWriter is written by one goroutine only).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread timeout.go Outdated
Comment thread README.md Outdated
Enforces a maximum request duration: the next handler runs in a goroutine with a
context deadline, and if it has not finished by the deadline the middleware responds
with 504 Gateway Timeout at the deadline — even for a handler that ignores the context.

Modeled on net/http.TimeoutHandler: the handler's output is buffered and written
through on success; if the deadline fires first the buffered output is discarded, a
504 is sent, and further handler writes return http.ErrHandlerTimeout. A canceled
parent context stops the handler without sending a 504. Panics propagate. Because the
response is buffered, http.Flusher and http.Hijacker are not supported (documented).
@paskal paskal force-pushed the timeout-middleware branch from ed95096 to 00c5b21 Compare July 1, 2026 00:35
@paskal

paskal commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Re the latest points (now outdated after the rewrite): the Flush/Hijack-commit, deferred-504-after-return, 1xx and 101 concerns are all resolved by the move to the enforcing/buffered model (there is no committed-tracking or deferred-504-after-commit anymore, and Flush/Hijack are intentionally not supported). The README now matches the behaviour. I did add the remaining valid point — a non-positive timeout now disables the middleware (calls the handler directly, no 504), like chi's timeout <= 0 no-op — with a test.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

Comment thread timeout.go
Comment on lines +67 to +76
case <-ctx.Done():
tw.mu.Lock()
defer tw.mu.Unlock()
tw.timedOut = true // discard the buffer and stop further handler writes
// only the deadline yields a 504; a canceled parent means the request is
// being abandoned, so there is nobody to send a status to
if ctx.Err() == context.DeadlineExceeded {
w.WriteHeader(http.StatusGatewayTimeout)
}
}
Comment thread timeout_test.go
Comment on lines +82 to +85
assert.Equal(t, http.StatusGatewayTimeout, rec.Code)
assert.Empty(t, rec.Body.String(), "the handler's buffered output must be discarded")
assert.Less(t, elapsed, 2*time.Second, "ServeHTTP must return at the deadline, not wait for the handler")

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants