Summary
When the governor rejects a request, it computes how long the client must wait and then discards it. The 429 response has no Retry-After header and no x-ratelimit-* headers, so clients cannot back off correctly. The x-ratelimit-* headers on successful responses are placeholders: remaining is always limit - 1 and reset is always now + 60s.
Version checked
acton-service 0.43.1 (crates.io source), observed through SchemaForge v0.45.0.
Source
src/middleware/governor.rs:393-405 (check_with_limiter): retry_after is computed from not_until.wait_time_from(...), logged, and then Err(Error::RateLimitExceeded) is returned without it. The RateLimitExceeded struct (:135-158) already models retry_after but is not used on this path.
src/error.rs:584-591: Error::RateLimitExceeded maps to a 429 JSON body with no headers.
src/middleware/governor.rs:386-390 and :424-447: remaining = requests_per_minute.saturating_sub(1) and reset_secs: 60 are constants, and the headers are only added to successful responses.
- By contrast,
src/lockout/middleware.rs:110-120 sets Retry-After on its 423.
Reproduction
$ for i in $(seq 1 40); do curl -s -o /dev/null http://127.0.0.1:3000/health; done
$ curl -s -D - -o /dev/null http://127.0.0.1:3000/health | grep -i 'HTTP/\|retry\|ratelimit'
HTTP/1.1 429 Too Many Requests
Expected
A 429 from the governor carries Retry-After: <ceil(seconds)> (RFC 9110 §10.2.3) and, ideally, x-ratelimit-limit / x-ratelimit-remaining: 0 / x-ratelimit-reset. Successful responses report the real remaining capacity, or omit the header rather than report a constant.
Suggested fix
Return a rate-limit error that carries the wait duration (for example Error::RateLimited { retry_after: Duration, limit: u32 }), and set the headers in its IntoResponse. Use the limiter state for remaining (governor 0.10's StateInformationMiddleware returns a StateSnapshot with remaining_burst_capacity()), or drop x-ratelimit-remaining until it is accurate. Add a test asserting Retry-After on a 429.
Summary
When the governor rejects a request, it computes how long the client must wait and then discards it. The 429 response has no
Retry-Afterheader and nox-ratelimit-*headers, so clients cannot back off correctly. Thex-ratelimit-*headers on successful responses are placeholders:remainingis alwayslimit - 1andresetis always now + 60s.Version checked
acton-service 0.43.1 (crates.io source), observed through SchemaForge v0.45.0.
Source
src/middleware/governor.rs:393-405(check_with_limiter):retry_afteris computed fromnot_until.wait_time_from(...), logged, and thenErr(Error::RateLimitExceeded)is returned without it. TheRateLimitExceededstruct (:135-158) already modelsretry_afterbut is not used on this path.src/error.rs:584-591:Error::RateLimitExceededmaps to a 429 JSON body with no headers.src/middleware/governor.rs:386-390and:424-447:remaining = requests_per_minute.saturating_sub(1)andreset_secs: 60are constants, and the headers are only added to successful responses.src/lockout/middleware.rs:110-120setsRetry-Afteron its 423.Reproduction
Expected
A 429 from the governor carries
Retry-After: <ceil(seconds)>(RFC 9110 §10.2.3) and, ideally,x-ratelimit-limit/x-ratelimit-remaining: 0/x-ratelimit-reset. Successful responses report the real remaining capacity, or omit the header rather than report a constant.Suggested fix
Return a rate-limit error that carries the wait duration (for example
Error::RateLimited { retry_after: Duration, limit: u32 }), and set the headers in itsIntoResponse. Use the limiter state forremaining(governor 0.10'sStateInformationMiddlewarereturns aStateSnapshotwithremaining_burst_capacity()), or dropx-ratelimit-remaininguntil it is accurate. Add a test assertingRetry-Afteron a 429.