From c745dd69aea04c24e19f1a8d55063b83f7b91619 Mon Sep 17 00:00:00 2001 From: Ragnor Comerford Date: Fri, 8 May 2026 16:58:47 +0200 Subject: [PATCH] server: emit Retry-After header on 429 / 503 responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the doc-vs-code gap at api.rs:343 and lib.rs:344-355: the documentation claims `Retry-After` is set on TooManyRequests / ServiceUnavailable responses, but `IntoResponse for ApiError` emitted only `(StatusCode, Json(ErrorOutput))` — no header. Wires a constant `RETRY_AFTER_SECONDS = "60"` for both 429 and 503 codes. Plumbing per-RejectReason durations through is a follow-up; the admission rejects we surface today recover bounded by request handler duration rather than calendar wait, so a constant suffices. Pinned by `ingest_per_actor_admission_cap_returns_429`. Test now fully green: 1+ of 8 concurrent /ingest under cap=1 receives 429 with Retry-After: 60. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/omnigraph-server/src/lib.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/omnigraph-server/src/lib.rs b/crates/omnigraph-server/src/lib.rs index cb5ca41..ad559ab 100644 --- a/crates/omnigraph-server/src/lib.rs +++ b/crates/omnigraph-server/src/lib.rs @@ -467,10 +467,29 @@ fn summarize_merge_conflicts(conflicts: &[api::MergeConflictOutput]) -> String { format!("merge conflicts: {}{}", preview.join("; "), suffix) } +/// Constant `Retry-After` value (seconds) emitted on 429 / 503 responses. +/// Matches the doc claim at `ApiError::too_many_requests` and +/// `ApiError::service_unavailable`. Plumbing per-RejectReason durations +/// through is a follow-up; the admission rejects we surface today are +/// uniformly bounded by the in-flight cap recovery time, which is +/// dominated by request handler duration rather than calendar wait. +const RETRY_AFTER_SECONDS: &str = "60"; + impl IntoResponse for ApiError { fn into_response(self) -> Response { + let mut headers = axum::http::HeaderMap::new(); + if matches!( + self.code, + ErrorCode::TooManyRequests | ErrorCode::ServiceUnavailable + ) { + headers.insert( + axum::http::header::RETRY_AFTER, + axum::http::HeaderValue::from_static(RETRY_AFTER_SECONDS), + ); + } ( self.status, + headers, Json(ErrorOutput { error: self.message, code: Some(self.code),