Is there an existing issue for this?
How do you use Sentry?
Sentry Saas (sentry.io)
Which SDK are you using?
@sentry/node (also reproduced through @sentry/nextjs 11.0.0 with Turbopack)
SDK Version
11.0.0
Framework Version
openai 7.23.0, Node.js 24.20.0
Steps to Reproduce
With the default openAIIntegration active, client.chat.completions.parse() and client.responses.parse() always throw TypeError: Body is unusable: Body has already been read. create() works.
Self-contained repro (no network, fake fetch):
// repro.mjs — run with `node repro.mjs`
import * as Sentry from '@sentry/node';
Sentry.init({ dsn: 'https://public@o0.ingest.sentry.io/0', tracesSampleRate: 1.0 });
const { default: OpenAI } = await import('openai');
const body = JSON.stringify({
id: 'x', object: 'chat.completion', created: 0, model: 'gpt-5-mini',
choices: [{ index: 0, finish_reason: 'stop', message: { role: 'assistant', content: '{"a":1}', refusal: null } }],
});
const client = new OpenAI({
apiKey: 'test',
fetch: async () => new Response(body, { headers: { 'content-type': 'application/json' } }),
});
const params = {
model: 'gpt-5-mini',
messages: [{ role: 'user', content: 'hi' }],
response_format: { type: 'json_schema', json_schema: { name: 'x', schema: { type: 'object' } } },
};
await Sentry.startSpan({ name: 'repro' }, async () => {
try { await client.chat.completions.create(params); console.log('create ok'); }
catch (e) { console.log('create FAIL:', e.message); }
try { await client.chat.completions.parse(params); console.log('parse ok'); }
catch (e) { console.log('parse FAIL:', e.message); }
});
| Setup |
create() |
parse() |
| No Sentry |
ok |
ok |
Sentry.init defaults |
ok |
FAIL: Body is unusable: Body has already been read |
Sentry.init with integrations: (d) => d.filter((i) => i.name !== 'OpenAI') |
ok |
ok |
client.responses.parse() fails the same way.
Expected Result
parse() resolves with the parsed completion, the same as without Sentry.
Actual Result
TypeError: Body is unusable: Body has already been read
at consumeBody (node:internal/deps/undici/undici)
at _Response.text (node:internal/deps/undici/undici)
at defaultParseResponse (openai/internal/parse.mjs)
at OpenAI.parseResponseWithTimeout (openai/client.mjs)
...
Analysis
The orchestrion wrapPromise transform wraps Completions.create / Responses.create. For non-native thenables it side-chains promise.then(...) and returns the original promise, so that APIPromise methods such as withResponse() stay available.
APIPromise parses lazily, though: calling .then() starts parse(), which reads response.text(). The SDK's parse() helpers are create(body)._thenUnwrap(transform), and in openai 7.x _thenUnwrap builds a new APIPromise over the same underlying Response, with its own parse step. Sentry's side-chained .then reads the body first, so the promise the caller actually awaits then reads a consumed body.
The same would apply to any other SDK helper built on _thenUnwrap over an instrumented create.
Workaround: await client.chat.completions.create(params) and pass the result to parseChatCompletion(completion, params) from openai/lib/parser (the function parse() uses internally). There is then only one APIPromise, so Sentry's .then and the caller share its cached parse. Disabling the OpenAI integration also works.
Additional Context
A fix probably needs the instrumentation to observe the result without triggering the APIPromise's parse, for example by hooking the parse result instead of calling .then() on the returned promise, or by also covering _thenUnwrap.
Is there an existing issue for this?
How do you use Sentry?
Sentry Saas (sentry.io)
Which SDK are you using?
@sentry/node (also reproduced through @sentry/nextjs 11.0.0 with Turbopack)
SDK Version
11.0.0
Framework Version
openai 7.23.0, Node.js 24.20.0
Steps to Reproduce
With the default
openAIIntegrationactive,client.chat.completions.parse()andclient.responses.parse()always throwTypeError: Body is unusable: Body has already been read.create()works.Self-contained repro (no network, fake
fetch):create()parse()Sentry.initdefaultsSentry.initwithintegrations: (d) => d.filter((i) => i.name !== 'OpenAI')client.responses.parse()fails the same way.Expected Result
parse()resolves with the parsed completion, the same as without Sentry.Actual Result
Analysis
The orchestrion
wrapPromisetransform wrapsCompletions.create/Responses.create. For non-native thenables it side-chainspromise.then(...)and returns the original promise, so thatAPIPromisemethods such aswithResponse()stay available.APIPromiseparses lazily, though: calling.then()startsparse(), which readsresponse.text(). The SDK'sparse()helpers arecreate(body)._thenUnwrap(transform), and in openai 7.x_thenUnwrapbuilds a newAPIPromiseover the same underlyingResponse, with its own parse step. Sentry's side-chained.thenreads the body first, so the promise the caller actually awaits then reads a consumed body.The same would apply to any other SDK helper built on
_thenUnwrapover an instrumentedcreate.Workaround:
await client.chat.completions.create(params)and pass the result toparseChatCompletion(completion, params)fromopenai/lib/parser(the functionparse()uses internally). There is then only oneAPIPromise, so Sentry's.thenand the caller share its cached parse. Disabling theOpenAIintegration also works.Additional Context
A fix probably needs the instrumentation to observe the result without triggering the
APIPromise's parse, for example by hooking the parse result instead of calling.then()on the returned promise, or by also covering_thenUnwrap.