Skip to content

fix(worker): resolve 'Body has already been used' error on retry#5573

Closed
colegottdank wants to merge 5 commits intomainfrom
fix/body-already-used-retry-error
Closed

fix(worker): resolve 'Body has already been used' error on retry#5573
colegottdank wants to merge 5 commits intomainfrom
fix/body-already-used-retry-error

Conversation

@colegottdank
Copy link
Collaborator

Summary

  • Fixes "Body has already been used" 500 errors that occurred when server-side retries were triggered (for 429 or 5xx responses from providers)
  • Converts ReadableStream body to string before the retry loop so it can be safely reused

Problem

When callProviderWithRetry() retried requests after receiving 429 or 5xx responses from providers, the retry would fail with "Body has already been used" errors. This happened because:

  1. The body in callProps could be a ReadableStream (when RequestBodyBuffer_Remote is used for large requests)
  2. ReadableStream bodies can only be consumed once by fetch()
  3. The first retry attempt consumed the stream, making it unusable for subsequent attempts

Solution

Convert any ReadableStream body to a string at the start of callProviderWithRetry() before entering the retry loop. Strings can be safely reused across multiple fetch() calls.

Test plan

  • TypeScript check passes
  • All 1421 worker tests pass

🤖 Generated with Claude Code

colegottdank and others added 5 commits February 16, 2026 21:37
Previously, reasoning_effort "medium" was incorrectly mapped to
thinkingLevel "low" for all Gemini 3 models. This caused inconsistent
thinking behavior compared to using Vertex directly.

Google's API supports "low", "medium", "high" for thinkingLevel
(Flash also supports "minimal"), which matches OpenAI's reasoning_effort
values. Now we pass through the value directly instead of mapping.

- Remove unnecessary mapping function
- Pass reasoning_effort directly as thinkingLevel for Gemini 3+ models
- Add "medium" to GeminiThinkingConfig type
- Update tests to reflect pass-through behavior

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The documentation interface was missing the "medium" thinking level
option that is supported by Gemini 3 Flash models.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The "minimal" thinking level is supported by Gemini Flash models
but was missing from GeminiThinkingConfig and GoogleThinkingConfig.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Added best practices for modifying TypeScript types:
- Run tsc --noEmit before committing
- Search for related types when updating union types
- Check cross-package dependencies
- Run full build to catch type errors

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When callProviderWithRetry() retried requests after 429 or 5xx responses,
it would fail with "Body has already been used" errors. This occurred
because ReadableStream bodies can only be consumed once by fetch().

The fix converts any ReadableStream body to a string before entering
the retry loop, allowing the body to be safely reused across retry
attempts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Feb 17, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
helicone Building Building Preview, Comment Feb 17, 2026 6:38am
helicone-bifrost Building Building Preview, Comment Feb 17, 2026 6:38am
helicone-eu Building Building Preview, Comment Feb 17, 2026 6:38am

Request Review

@supabase
Copy link

supabase bot commented Feb 17, 2026

This pull request has been ignored for the connected project bolqqmqbrciybnypvklh because there are no changes detected in supbase/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@supabase
Copy link

supabase bot commented Feb 17, 2026

This pull request has been ignored for the connected project lmahfbbnchpworytrrqk because there are no changes detected in .supabase/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@claude
Copy link
Contributor

claude bot commented Feb 17, 2026

Claude finished @colegottdank's task —— View job


Code Review Complete

Score: 8/10 - Good to merge, minor suggestions for improvement

This PR successfully fixes the "Body has already been used" error in the worker's retry logic. The core fix is solid and the approach is correct.

Core Fix Analysis (ProviderClient.ts)

Problem identification: Correctly identified that ReadableStream bodies can only be consumed once by fetch()

Solution approach: Converting ReadableStream to string before retry loop is the right approach

Implementation: Clean implementation with proper error handling

Minor suggestions for the core fix:

  1. Memory efficiency consideration (lines 194-204): For very large request bodies, converting the entire ReadableStream to a string could consume significant memory. Consider adding a size check or streaming approach for extremely large payloads.

  2. Error handling: Consider wrapping the ReadableStream reading logic in a try-catch to handle potential stream read errors gracefully.

  3. Type safety: The retryableBody variable could benefit from more explicit typing to ensure type safety.

Google Provider Changes Analysis

The Google provider changes appear to be related to adding support for reasoning/thinking features:

Type additions: Added "minimal" and "medium" to thinkingLevel union types - proper type system updates

Test coverage: Comprehensive test coverage for the reasoning functionality in google-reasoning.test.ts

Documentation: Good inline comments explaining the reasoning configuration logic

Observations on Google changes:

  • The changes are well-structured and follow existing patterns
  • Test coverage is thorough with edge cases covered
  • The mapping logic between OpenAI and Google formats looks correct

Potential Issues Found

Minor Performance Issue: In ProviderClient.ts line 203, the string concatenation approach could be optimized:

// Current approach
retryableBody = chunks.map((chunk) => decoder.decode(chunk)).join("");

// More efficient approach would be:
const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
const combined = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
  combined.set(chunk, offset);
  offset += chunk.length;
}
retryableBody = decoder.decode(combined);

Type Safety Enhancement: Consider adding runtime validation for the body conversion to ensure the conversion doesn't fail silently.

Security & Quality Assessment

No security issues identified
No breaking changes
Backward compatible
Proper error boundaries maintained
Good test coverage for new functionality

Suggestions Summary:

Critical fixes needed: None

Recommended improvements:

  • Add error handling around ReadableStream reading
  • Consider memory optimization for very large payloads
  • Optimize string concatenation for better performance

Optional enhancements:

  • Add runtime validation for body conversion
  • Consider adding metrics/logging for retry scenarios with body conversion

The fix addresses the core issue effectively and the Google provider enhancements are well-implemented. The code quality is good and follows project conventions.

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

Your free trial has ended. If you'd like to continue receiving code reviews, you can add a payment method here.

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.

1 participant