DUX-8094: Re-sign Bedrock requests on retry to fix SigV4 expiry#15
DUX-8094: Re-sign Bedrock requests on retry to fix SigV4 expiry#15samuelboland wants to merge 1 commit into
Conversation
Bedrock requests are signed once before the first attempt. SigV4 signatures embed a timestamp and expire after 5 minutes, so faraday-retry would resend the same stale signature on every retry, guaranteeing a "Signature expired" error on any retry that follows a slow first attempt. Hook faraday-retry's retry_block to recompute the signature with a fresh X-Amz-Date before each replay. Other providers that don't define #sign_headers are unaffected. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Sam Boland <sam.boland@appfolio.com>
|
Jira Issue: https://appfolio.atlassian.net/browse/DUX-8094 |
|
🤖 Supernova Code Review — View trace |
|
Fix me a full review of this PR. |
There was a problem hiding this comment.
Review summary
The approach is sound — intercepting retries to refresh the SigV4 signature before replay is the right place to fix the 5-minute expiry problem.
One concrete issue to address before merging:
The resign_bedrock_retry(env:, **) keyword-argument signature requires faraday-retry ≥ 2.0 (see inline comment on line 120). The gemspec currently allows >= 1, and 1.x calls retry_block with five positional arguments, which will raise ArgumentError on every retry. Bumping the gemspec minimum to >= 2.0 (or adding a compatibility shim) is needed.
Minor observation (spec):
The test manually signs the initial request in the Faraday block rather than going through the production signing path (the protocol code). The retry re-sign logic is still exercised correctly, but the asymmetry between the test's initial signing and production's initial signing could mask subtle differences in what env.body looks like at retry time. Either using provider.signed_post for the test call or adding a clarifying comment would help.
| # retry_block runs synchronously, still inside the retry middleware, right before the env | ||
| # is replayed, so recomputing the signature here re-signs each attempt with a fresh | ||
| # X-Amz-Date. Other providers don't define #sign_headers and are unaffected. | ||
| def resign_bedrock_retry(env:, **) |
There was a problem hiding this comment.
The (env:, **) keyword-argument signature only works with faraday-retry ≥ 2.0. faraday-retry 2.0 changed retry_block from five positional arguments (env, options, retries, exception, will_retry_in) to keyword arguments. With the current gemspec constraint of faraday-retry >= 1, anyone running 1.x will hit ArgumentError: wrong number of arguments (given 5, expected 0; required keyword: env) on every retry attempt, replacing the SigV4 bug with a different crash.
The gemspec minimum should be bumped to >= 2.0:
# ruby_llm.gemspec
spec.add_dependency 'faraday-retry', '>= 2.0'| request_headers = stub_timeout_then_success(connection) | ||
|
|
||
| body = '{"a":1}' | ||
| response = connection.post('/model/x/converse', { a: 1 }) do |req| |
There was a problem hiding this comment.
The test manually applies sign_headers in the request block on every call. In production, the Bedrock protocol code applies the initial signature via its own signing path — the retry re-sign uses env.body as it exists at retry time (already JSON-encoded by the :json middleware). This test therefore doesn't cover the exact production signing path for the initial attempt. Consider replacing the manual sign_headers block with a production-style signed POST (e.g. provider.signed_post(...)) or adding a comment clarifying the deliberate simplification, so future readers don't confuse the test setup with a production call.
Problem
AWS SigV4 signatures embed a timestamp and expire after 5 minutes. When
faraday-retrymiddleware retries a timed-out Bedrock request, it resends the original request with the stale SigV4 signature. If the retry occurs after 5 minutes, AWS rejects it with a 403 InvalidSignatureException, which gets converted toRubyLLM::ForbiddenError.Solution
Added a
resign_bedrock_retrycallback to the retry middleware that re-signs Bedrock requests before each retry attempt. This hook:Changes
retry_blockto retry middleware config and implementedresign_bedrock_retrymethod that detects Bedrock requests and re-signs themAgent session: https://staging.supernova.dx.appf.io/coders/93bb1211-0236-475a-be52-aaf5b95d2ea6