You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The OpenAI integration raises out of the instrumented create() call when a response object has a None collection where the integration expects an iterable. The exception is not contained by the SDK, so it surfaces as an application error and replaces whatever the API actually returned.
This is reachable in practice through OpenAI-compatible gateways. When a gateway has already flushed 200 OK (for example after sending keep-alive padding while it buffers a large request) and only then learns the upstream call failed, it can no longer change the status code, so it reports the failure in the body:
{"error": {"message": "... At least one of the image dimensions exceed max allowed size: 8000 pixels", "code": 400}}
The openai client parses that into a model object with no output / choices populated, and instrumenting it raises.
Minimal reproduction, no network needed:
fromunittestimportmockimportsentry_sdkfromopenaiimportOpenAIfromopenai.types.responsesimportResponsefromsentry_sdk.integrations.openaiimportOpenAIIntegrationsentry_sdk.init(
dsn="https://public@o1.ingest.sentry.io/1",
traces_sample_rate=1.0,
integrations=[OpenAIIntegration()],
transport=lambdaevent: None,
)
# What the client parses out of a 200 response that only carries an errorgateway_error=Response.construct(
error={"message": "upstream failed", "code": 400},
output=None,
)
client=OpenAI(api_key="z")
client.responses._post=mock.Mock(return_value=gateway_error)
withsentry_sdk.start_transaction(name="tx"):
response=client.responses.create(model="gpt-4o", input="hello")
print(response.error)
Expected Result
create() returns the response object, and the application can read response.error to see what actually went wrong. Instrumentation problems stay inside the SDK.
Actual Result
File "sentry_sdk/integrations/openai.py", line 1532, in _new_sync_responses_create
_set_responses_api_output_data(
File "sentry_sdk/integrations/openai.py", line 1275, in _set_responses_api_output_data
_set_common_output_data(
File "sentry_sdk/integrations/openai.py", line 746, in _set_common_output_data
for output in response.output:
^^^^^^^^^^^^^^^
TypeError: 'NoneType' object is not iterable
The application never sees the real error message, so any retry/fallback logic keyed on it stops working.
There appear to be two separate causes:
response.output is not checked for None.response.choices was given an is not None check after OpenAI integration fails when response has no messages in choices. #5071, but the Responses API branch still does a bare hasattr(response, "output") in _set_common_output_data and _calculate_responses_token_usage.
The non-streaming paths lost their capture_internal_exceptions() wrapper. Before OpenAI integration update #4612 the whole response-handling block ran inside with capture_internal_exceptions():. The restructuring into _set_*_input_data / _set_*_output_data moved that code out from under the wrapper, so any error there now propagates into user code. The streaming iterators kept their protection; the non-streaming paths did not. This is why <2.34 is unaffected.
The second point is the more general one — with it fixed, this class of bug degrades to a missing span attribute rather than an application crash, which matches the SDK contract in CONTRIBUTING.md ("Users do not expect their application to crash").
I have a patch with tests for both points and am happy to open a PR if this looks like the right direction.
How do you use Sentry?
Sentry Saas (sentry.io)
Version
2.68.0 (also reproduces on 2.35.1 and 2.40.0)
Steps to Reproduce
The OpenAI integration raises out of the instrumented
create()call when a response object has aNonecollection where the integration expects an iterable. The exception is not contained by the SDK, so it surfaces as an application error and replaces whatever the API actually returned.This is reachable in practice through OpenAI-compatible gateways. When a gateway has already flushed
200 OK(for example after sending keep-alive padding while it buffers a large request) and only then learns the upstream call failed, it can no longer change the status code, so it reports the failure in the body:{"error": {"message": "... At least one of the image dimensions exceed max allowed size: 8000 pixels", "code": 400}}The
openaiclient parses that into a model object with nooutput/choicespopulated, and instrumenting it raises.Minimal reproduction, no network needed:
Expected Result
create()returns the response object, and the application can readresponse.errorto see what actually went wrong. Instrumentation problems stay inside the SDK.Actual Result
The application never sees the real error message, so any retry/fallback logic keyed on it stops working.
There appear to be two separate causes:
response.outputis not checked forNone.response.choiceswas given anis not Nonecheck after OpenAI integration fails when response has no messages in choices. #5071, but the Responses API branch still does a barehasattr(response, "output")in_set_common_output_dataand_calculate_responses_token_usage.The non-streaming paths lost their
capture_internal_exceptions()wrapper. Before OpenAI integration update #4612 the whole response-handling block ran insidewith capture_internal_exceptions():. The restructuring into_set_*_input_data/_set_*_output_datamoved that code out from under the wrapper, so any error there now propagates into user code. The streaming iterators kept their protection; the non-streaming paths did not. This is why<2.34is unaffected.The second point is the more general one — with it fixed, this class of bug degrades to a missing span attribute rather than an application crash, which matches the SDK contract in CONTRIBUTING.md ("Users do not expect their application to crash").
I have a patch with tests for both points and am happy to open a PR if this looks like the right direction.