Skip to content

Fix Bedrock streaming dropping all chunks when Faraday env is nil#813

Merged
crmne merged 5 commits into
crmne:mainfrom
wistia:anders/fix-bedrock-streaming-nil-env
Jul 7, 2026
Merged

Fix Bedrock streaming dropping all chunks when Faraday env is nil#813
crmne merged 5 commits into
crmne:mainfrom
wistia:anders/fix-bedrock-streaming-nil-env

Conversation

@chen-anders

@chen-anders chen-anders commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

What this does

Fixes a bug where Bedrock ConverseStream responses come back completely empty (zero chunks, blank content, nil token counts) even though the HTTP request succeeds with status 200.

The Faraday 2 on_data callback gates chunk parsing on env&.status == 200. With the net_http adapter, env is nil during streaming (the status is not yet known when chunks arrive), so this condition is false for every chunk. Each valid AWS event-stream frame is then routed to the failed-response handler, which tries to JSON.parse binary eventstream bytes, fails, logs a "failed stream error chunk" debug line, and silently drops it. The result is an empty streamed response.

This affects any app on Faraday 2.x + the net_http adapter (Faraday's default) using Bedrock streaming.

Fix: invert the guard so a nil env (status unknown) falls through to normal chunk parsing, and only a present env reporting a non-200 status is treated as a failure. Applied in both spots that build the v2 on_data proc:

  • RubyLLM::Streaming::FaradayHandlers.v2_on_data (shared SSE handler)
  • RubyLLM::Protocols::Converse::Streaming#stream_response (Bedrock ConverseStream)

Reproduction

chat = RubyLLM.chat(model: "us.anthropic.claude-sonnet-4-6", provider: "bedrock", assume_model_exists: true)

# Non-streaming — works:
chat.ask("Say BANANA only.").content          # => "BANANA"

# Streaming — broken before this fix:
chunks = []
resp = chat.ask("Say BANANA only.") { |c| chunks << c }
chunks.size    # => 0   (expected: > 0)
resp.content   # => ""   (expected: "BANANA")

With Faraday 2.x + net_http, the debug log shows the data arriving fine ({"delta":{"text":"BANANA"}}, usage tokens, status 200) but every frame hitting "Failed Bedrock stream error chunk". After the fix: chunks.size => 5, resp.content => "BANANA", tokens populated.

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • Performance improvement

Scope check

  • I read the Contributing Guide
  • This aligns with RubyLLM's focus on LLM communication
  • This isn't application-specific logic that belongs in user code
  • This benefits most users, not just my specific use case

Quality check

  • I ran overcommit --install and all hooks pass
  • I tested my changes thoroughly
    • For provider changes: Re-recorded VCR cassettes with bundle exec rake vcr:record[provider_name]
    • Added unit tests covering nil-env / 200 / non-200 routing for both code paths (spec/ruby_llm/streaming_spec.rb, spec/ruby_llm/protocols/converse/streaming_spec.rb)
  • I updated documentation if needed
  • I didn't modify auto-generated files manually (models.json, aliases.json)

AI-generated code

  • I used AI tools to help write this code
  • I have reviewed and understand all generated code (required if above is checked)

API changes

  • Breaking change
  • New public methods/classes
  • Changed method signatures
  • No API changes

@codecov

codecov Bot commented Jun 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.34%. Comparing base (3998b05) to head (a0f7bf5).

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #813      +/-   ##
==========================================
+ Coverage   83.29%   83.34%   +0.05%     
==========================================
  Files         172      172              
  Lines        8281     8282       +1     
  Branches     1347     1347              
==========================================
+ Hits         6898     6903       +5     
- Misses        887      888       +1     
+ Partials      496      491       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chen-anders chen-anders force-pushed the anders/fix-bedrock-streaming-nil-env branch from 8321563 to a1093e9 Compare June 16, 2026 03:50

@crmne crmne left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

The bug fix and test coverage look right. Please tighten the shape before merging: keep the Faraday status-routing rule in one helper, and keep the comment/predicate small.

Comment thread lib/ruby_llm/streaming.rb Outdated
if env&.status == 200
on_chunk.call(chunk, env)
else
# A nil env means the status is not yet known (Faraday 2 with the

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Let's keep this short, and treat unknown status the same whether env is nil or env.status is nil.

# Some adapters do not expose response status during on_data.
status = env&.status

if status && status != 200
  on_failed_response.call(chunk, env)
else
  on_chunk.call(chunk, env)
end

if env&.status == 200
parse_stream_chunk(decoder, chunk, accumulator, &block)
else
# A nil env means the status is not yet known (Faraday 2 with the

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Converse needs its own AWS Event Stream parser, but it can still use the existing Faraday on_data wrapper so this routing rule lives in one place.

faraday_v1 = Faraday::VERSION.start_with?('1')
on_data = RubyLLM::Streaming::FaradayHandlers.build(
  faraday_v1: faraday_v1,
  on_chunk: ->(chunk, _env) { parse_stream_chunk(decoder, chunk, accumulator, &block) },
  on_failed_response: ->(chunk, env) { handle_failed_stream(chunk, env) }
)

if faraday_v1
  req.options[:on_data] = on_data
else
  req.options.on_data = on_data
end

chen-anders and others added 3 commits July 8, 2026 01:56
The Faraday 2 on_data callback gates chunk parsing on
`env&.status == 200`. With the net_http adapter, env is nil during
streaming (the status is not yet known), so this is false for every
chunk. Each valid AWS event-stream frame is then routed to the
failed-response handler, which tries to JSON-parse binary eventstream
bytes, fails, logs a "failed stream error chunk" debug line, and drops
it. The result: ConverseStream responses come back completely empty
(zero chunks, blank content, nil token counts) even though the HTTP
request itself succeeds with status 200.

Invert the guard so a nil env (status unknown) falls through to normal
chunk parsing, and only a present env reporting a non-200 status is
treated as a failure. Applied in both the Converse streaming module and
the shared FaradayHandlers.v2_on_data helper.
Cover the regression where a nil Faraday env (Faraday 2 + net_http
during streaming) caused every chunk to be routed to the
failed-response handler and discarded. Tests exercise both the shared
FaradayHandlers.v2_on_data helper and the Converse stream_response
on_data proc directly, asserting that a nil env and a 200 env both
parse the chunk while a present non-200 env triggers failure handling.
- Use `status = env&.status` pattern in FaradayHandlers#v2_on_data so nil
  env and nil status are treated identically with a shorter comment
- Refactor Converse::Streaming to use FaradayHandlers.build instead of
  duplicating the Faraday status-routing logic inline

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chen-anders chen-anders force-pushed the anders/fix-bedrock-streaming-nil-env branch from 49b5043 to 9fa1fc4 Compare July 7, 2026 19:02
chen-anders and others added 2 commits July 8, 2026 02:21
Inline let helpers as local variables in the FaradayHandlers tests to
keep the memoized helper count within the configured limit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
stream_url calls escape_model_id which is defined in Converse::Chat,
not Converse::Streaming. Add a singleton method stub on the plain
Object test double so the on_data routing specs can exercise
stream_response without needing the full Chat module.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@chen-anders chen-anders requested a review from crmne July 7, 2026 19:28
@crmne crmne merged commit b229895 into crmne:main Jul 7, 2026
23 checks passed
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.

2 participants