Repository navigation
feat: per-route timeout and throttle (#140) - #253
Merged
Merged
Conversation
… parsing Adds per-route Timeout (time.Duration) and Throttle (int) fields to the URLMapper struct. Zero values preserve existing global-fallback behavior. extendMapper now propagates both fields through the simple-extension path so routes like /api/ -> /dst/ don't silently lose per-route config before reaching Match(). File (yaml) provider parses timeout as a duration string and throttle as int. Negative timeout/throttle values surface as parse errors. Part of plan 20260512-per-route-timeout-throttle.md (Task 1 of 10).
Extend rule format from server,source,dest[,ping[,forward-health-checks]] to server,source,dest[,ping[,forward-health-checks[,timeout[,throttle]]]]. Empty positional fields inherit global defaults; invalid duration, non-integer throttle, or negative values return parse errors at startup.
Read reproxy.<n>.timeout and reproxy.<n>.throttle labels via the existing labelN helper. Invalid duration, non-integer, or negative values warn and fall back to zero (matches the warn-and-zero pattern of forward-health-checks/keep-host), so a single bad label cannot crash discovery for the entire fleet.
Parse reproxy.timeout and reproxy.throttle labels with warn-and-zero semantics matching the docker provider behavior.
When a matched URLMapper has Timeout > 0, wrap the request context with context.WithTimeout and override the connection's read/write deadlines via http.ResponseController. The middleware runs immediately after matchHandler and before per-route auth/throttle so auth and limiter work runs inside the per-route deadline. http.ErrNotSupported from ResponseController is treated as expected (context timeout still propagates).
When matched route has Throttle > 0, apply a per-route rate limiter keyed by [ip, dst] so per-user behavior is preserved while the route gets its own bucket. Cache route limiters in a sync.Map keyed by server|src|rate; including rate in the key means config edits produce new limiters on next request. Build the global limiter lazily via sync.Once and drop the early-return to passThroughHandler when global rate is 0 — that path now still consults per-route Throttle, so route overrides work even when the global throttle is disabled.
Add integration test that wires up a real Http server with provider.File pointing at a fixture with four routes (timeout, throttle, both, control). Tests verify per-route timeout fires and returns 502, throttle blocks excess rapid requests with 429, chain ordering keeps throttle 429 inside the deadline window, and control route remains unaffected. While building the test, two behaviors needed implementation fixes: - routeTimeoutHandler now sets the write deadline 500ms after the ctx deadline (writeDeadlineGrace) so the proxy's default ErrorHandler can flush 502 to the client before the conn is severed; without the grace ctx and conn-write fire simultaneously and the client sees EOF instead of the documented 502. - Both read and write deadlines are reset to zero in a defer so kept-alive connections do not carry expired deadlines into subsequent requests. Updated the Task 5 deadlines unit test to verify both the arm and the deferred clear, and the plan file to document the discovered behaviors.
Add provider-specific documentation for the new per-route timeout and throttle options across file, static, docker, and consul-catalog providers. Add a "Per-route timeout and throttle" subsection covering global-vs-per-route precedence, the write-deadline override, and the response-header-timeout limitation.
Mark Task 10 final steps complete and relocate the plan file under docs/plans/completed/ now that all implementation, tests, README updates, and acceptance verification have landed.
Rename local variable in file.List() from d to dur to avoid shadowing the *File receiver d. The shadowing was harmless in scope (no receiver calls inside the block) but flagged by smells analysis as a readability issue inconsistent with the consulcatalog provider, which already uses dur for the same parsed duration.
- handlers.go: use NUL separator in route limiter cache key to avoid collisions when regex source contains pipe characters - handlers.go: drop unnecessary sync.Once for global limiter; build eagerly at handler construction - handlers.go: log non-ErrNotSupported errors when clearing connection deadlines, consistent with the setter side - handlers.go: expand godoc on limiterUserHandler and routeTimeoutHandler to document key composition and connection-deadline side effects - file.go: replace contradictory "can't parse N: negative duration" error wording with the static provider's non-negative phrasing - consulcatalog.go: move timeout/throttle label parsing below the enabled check so disabled services don't emit parse warnings - README.md: clarify that empty positional fields skip optional values rather than inherit non-existent global defaults - CLAUDE.md: list per-route overrides on the URLMapper summary
Contributor
There was a problem hiding this comment.
Pull request overview
Adds per-route overrides for request timeout and per-user throttling in reproxy’s routing model (URLMapper), allowing operators to tune slow endpoints and sensitive routes without changing global defaults. The change is implemented end-to-end across all discovery providers, enforced in the proxy middleware chain, and covered with unit + integration tests.
Changes:
- Extend
discovery.URLMapperwithTimeoutandThrottle(zero = inherit global), ensuringextendMapperpreserves them. - Add provider support for parsing/reading per-route
timeoutandthrottle(file/static/docker/consul-catalog) with provider-appropriate validation behavior. - Enforce per-route timeout and per-route throttle in the proxy via new/updated middleware, with unit and integration tests plus README/CLAUDE.md updates.
Reviewed changes
Copilot reviewed 19 out of 19 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Documents per-route timeout/throttle syntax for all providers and the response-header-timeout limitation. |
| docs/plans/completed/20260512-per-route-timeout-throttle.md | Adds the completed implementation plan/notes for the feature (design + testing approach). |
| CLAUDE.md | Updates architecture notes to mention the new per-route overrides in URLMapper. |
| app/proxy/testdata/per_route.yml | Adds integration test fixture routes covering timeout/throttle/both/control. |
| app/proxy/proxy.go | Wires routeTimeoutHandler into the middleware chain immediately after route matching. |
| app/proxy/proxy_test.go | Adds end-to-end integration test validating per-route timeout (502) and per-route throttle (429). |
| app/proxy/handlers.go | Implements routeTimeoutHandler and updates limiterUserHandler to support per-route limiter overrides. |
| app/proxy/handlers_test.go | Adds unit tests for routeTimeoutHandler and per-route throttle behavior in limiterUserHandler. |
| app/discovery/provider/testdata/config.yml | Extends file-provider fixture with routes using timeout and/or throttle. |
| app/discovery/provider/static.go | Extends static provider CSV rule format to include optional timeout and throttle fields. |
| app/discovery/provider/static_test.go | Adds static provider test cases for timeout/throttle parsing and validation errors. |
| app/discovery/provider/file.go | Adds YAML parsing for timeout/throttle with validation (non-negative, duration parsing). |
| app/discovery/provider/file_test.go | Extends file provider tests to assert parsed timeout/throttle and error cases. |
| app/discovery/provider/docker.go | Adds docker label parsing for reproxy.<n>.timeout / reproxy.<n>.throttle (warn-and-zero on bad values). |
| app/discovery/provider/docker_test.go | Adds docker provider tests for valid/invalid/negative timeout and throttle labels (including numbered routes). |
| app/discovery/provider/consulcatalog/consulcatalog.go | Adds consul-catalog tag parsing for reproxy.timeout / reproxy.throttle (warn-and-zero on bad values). |
| app/discovery/provider/consulcatalog/consulcatalog_test.go | Adds consul-catalog provider tests for timeout/throttle parsing and bad-value handling. |
| app/discovery/discovery.go | Adds Timeout/Throttle fields to URLMapper and preserves them through extendMapper. |
| app/discovery/discovery_test.go | Adds tests ensuring extendMapper preserves Timeout and Throttle for extension and non-extension routes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
adds optional per-route
timeoutandthrottlefields toURLMapper, falling back to global when zero. Addresses #140.per-route deadline uses
context.WithTimeout+http.ResponseController.SetReadDeadline/SetWriteDeadline, which overrides--timeout.writeon the connection for matched requests. Per-route throttle uses a lazysync.Mapof route-scoped tollbooth limiters; the[ip, dst]key shape is preserved so per-user throttling does not regress.all four providers gained the new fields:
timeout: 5m,throttle: 10forward-health-checksreproxy.<n>.timeout,reproxy.<n>.throttlelabels (warn-and-zero on bad values)reproxy.timeout,reproxy.throttletags (same warn-and-zero)zero value = inherit global, positive overrides.
extendMapperwas updated to carry both fields through implicit-extension routes (a place provider-level tests would not catch on their own).known limitation: per-route
timeoutdoes NOT override transport-level--timeout.resp-header(default 5s). OverridingTransport.ResponseHeaderTimeoutper route would require per-destination transports, which is intentionally out of scope for this PR. Operators with slow report-generation upstreams still need to raise--timeout.resp-headerglobally. README documents this.