Skip to content

fix(proxy): use e.request_data for logging_obj in ModifyResponseException streaming passthrough - #30800

Merged
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_fix_streaming_block_logging_obj
Jun 19, 2026
Merged

fix(proxy): use e.request_data for logging_obj in ModifyResponseException streaming passthrough#30800
mateo-berri merged 2 commits into
litellm_internal_stagingfrom
litellm_fix_streaming_block_logging_obj

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Relevant issues

Internal copy of #30701 by @seph-barker (Joseph Barker) so we can run it through CircleCI. Full credit for the fix and tests goes to the original author; the two commits here are cherry-picked from that PR with authorship preserved.

A guardrail that blocks a streaming request pre-call (by raising ModifyResponseException or RejectedRequestError) 500s the request instead of streaming the violation message back as a 200.

Pre-Submission checklist

  • I have added meaningful tests
  • My PR's scope is as isolated as possible; it only solves 1 specific problem

Type

🐛 Bug Fix

Changes

The bug

When a guardrail blocks a streaming request pre-call, chat_completion's except ModifyResponseException / except RejectedRequestError handlers stream the violation message back as a 200 by building a CustomStreamWrapper:

except ModifyResponseException as e:
    _data = e.request_data
    ...
    _streaming_response = litellm.CustomStreamWrapper(
        completion_stream=_iterator,
        model=e.model,
        custom_llm_provider="cached_response",
        logging_obj=data.get("litellm_logging_obj", None),   # <-- outer body
    )

data here is the outer request body returned by _read_request_body. It never carries litellm_logging_obj: the processor's data dict diverges from the outer body at function_setup (which returns a fresh kwargs dict), and litellm_logging_obj is attached only to that processor copy. So data.get("litellm_logging_obj") is None, and CustomStreamWrapper.__init__ immediately dereferences it:

litellm_params: GenericLiteLLMParams = GenericLiteLLMParams(
    **self.logging_obj.model_call_details.get("litellm_params", {})
)
AttributeError: 'NoneType' object has no attribute 'model_call_details'

The non-streaming path is unaffected (it returns a ModelResponse directly), and the anthropic / responses-API passthrough paths are unaffected (they stream via a plain SSE generator, not CustomStreamWrapper). Only streaming + pre-call block hits this.

The fix

_data = e.request_data is already bound two lines above and is the processor's data dict, which does carry litellm_logging_obj. Read logging_obj from _data in both streaming passthrough handlers (ModifyResponseException and RejectedRequestError).

Added a regression test (tests/test_litellm/proxy/test_modify_response_streaming_passthrough.py) that drives chat_completion's ModifyResponseException streaming branch with an outer body lacking litellm_logging_obj and asserts the CustomStreamWrapper receives the logging object from e.request_data (it would be None without the fix).


Generated by Claude Code


Note

Low Risk
Narrow bug fix in guardrail streaming error handlers with targeted regression tests; no auth or data-model changes.

Overview
Fixes 500 errors when a guardrail blocks a streaming chat completion pre-call via ModifyResponseException or RejectedRequestError, where the intended behavior is to stream the violation as a 200 through CustomStreamWrapper.

In both streaming passthrough branches in chat_completion, logging_obj is now taken from _data (e.request_data) instead of the outer data from _read_request_body. Only the processor copy carries litellm_logging_obj; using the outer body left logging_obj as None and triggered AttributeError inside CustomStreamWrapper.

Adds regression tests that assert CustomStreamWrapper receives the logging object from the exception’s request_data when the outer body lacks it.

Reviewed by Cursor Bugbot for commit dd7db9a. Bugbot is set up for automated code reviews on this repo. Configure here.

…tion streaming passthrough

When a guardrail blocks a streaming request pre-call by raising
ModifyResponseException (or RejectedRequestError), chat_completion streams the
violation message back as a 200 by building a CustomStreamWrapper. It read the
logging object from the outer request body (`data.get("litellm_logging_obj")`),
but that dict never carries litellm_logging_obj -- it diverges from the
processor's data at function_setup, and only the processor copy (exposed as
e.request_data, already bound to `_data` here) gets the logging object
attached. CustomStreamWrapper.__init__ then dereferences
`logging_obj.model_call_details` on None and 500s the request with
"AttributeError: 'NoneType' object has no attribute 'model_call_details'".

Read logging_obj from `_data` (= e.request_data) in both streaming
passthrough handlers so the refusal streams correctly. The non-streaming and
the anthropic/responses passthrough paths were unaffected.

Adds a regression test asserting the wrapper receives the logging object from
e.request_data rather than None.
The streaming logging_obj fix was applied to both the ModifyResponseException
and RejectedRequestError handlers, but only the former had a regression test.
Extract a shared helper and add a parallel test for the RejectedRequestError
streaming path so both handlers stay guarded against the None-logging_obj crash.
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai


Generated by Claude Code

@greptile-apps

greptile-apps Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a 500 error triggered when a guardrail blocks a streaming chat-completion request pre-call via ModifyResponseException or RejectedRequestError. The root cause was that both streaming passthrough handlers sourced logging_obj from the outer data dict (from _read_request_body), which never carries litellm_logging_obj, causing CustomStreamWrapper to dereference None and crash.

  • In both exception handlers, logging_obj is now read from _data (e.request_data), which is the processor's data dict and does carry litellm_logging_obj.
  • A new regression test (test_modify_response_streaming_passthrough.py) drives both exception paths with a mock outer body that lacks litellm_logging_obj and asserts CustomStreamWrapper receives the sentinel from e.request_data; all tests are mock-only with no real network calls.

Confidence Score: 5/5

Safe to merge — two one-line changes in isolated exception handlers with targeted regression tests covering both paths.

The change is a two-line fix in well-understood exception handlers that only activate when a guardrail raises during streaming; the non-streaming path and all other routes are untouched. The added tests directly exercise the broken path and verify the fix without making real network calls.

No files require special attention.

Important Files Changed

Filename Overview
litellm/proxy/proxy_server.py Two one-line fixes: both streaming passthrough handlers (ModifyResponseException and RejectedRequestError) now read litellm_logging_obj from _data (e.request_data) instead of the outer request body data, which never carries that key.
tests/test_litellm/proxy/test_modify_response_streaming_passthrough.py New regression test file using mocks only; exercises both ModifyResponseException and RejectedRequestError streaming passthrough paths and asserts CustomStreamWrapper receives the logging object from e.request_data rather than None.

Reviews (2): Last reviewed commit: "test(proxy): cover RejectedRequestError ..." | Re-trigger Greptile

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run


Generated by Claude Code

@codecov

codecov Bot commented Jun 19, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit dd7db9a. Configure here.

@mateo-berri
mateo-berri requested a review from Sameerlite June 19, 2026 03:46
@mateo-berri
mateo-berri marked this pull request as ready for review June 19, 2026 03:46
@mateo-berri
mateo-berri merged commit f9b8b97 into litellm_internal_staging Jun 19, 2026
124 checks passed
@mateo-berri
mateo-berri deleted the litellm_fix_streaming_block_logging_obj branch June 19, 2026 06:29
fzowl pushed a commit to fzowl/litellm that referenced this pull request Jun 24, 2026
…tion streaming passthrough (BerriAI#30800)

* fix(proxy): use e.request_data for logging_obj in ModifyResponseException streaming passthrough

When a guardrail blocks a streaming request pre-call by raising
ModifyResponseException (or RejectedRequestError), chat_completion streams the
violation message back as a 200 by building a CustomStreamWrapper. It read the
logging object from the outer request body (`data.get("litellm_logging_obj")`),
but that dict never carries litellm_logging_obj -- it diverges from the
processor's data at function_setup, and only the processor copy (exposed as
e.request_data, already bound to `_data` here) gets the logging object
attached. CustomStreamWrapper.__init__ then dereferences
`logging_obj.model_call_details` on None and 500s the request with
"AttributeError: 'NoneType' object has no attribute 'model_call_details'".

Read logging_obj from `_data` (= e.request_data) in both streaming
passthrough handlers so the refusal streams correctly. The non-streaming and
the anthropic/responses passthrough paths were unaffected.

Adds a regression test asserting the wrapper receives the logging object from
e.request_data rather than None.

* test(proxy): cover RejectedRequestError streaming passthrough

The streaming logging_obj fix was applied to both the ModifyResponseException
and RejectedRequestError handlers, but only the former had a regression test.
Extract a shared helper and add a parallel test for the RejectedRequestError
streaming path so both handlers stay guarded against the None-logging_obj crash.

---------

Co-authored-by: Joseph Barker <joseph.barker@rubrik.com>
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.

3 participants