moeru-ai/airi · error · Error
Streaming transcription response is missing a readable body.
Error message
Streaming transcription response is missing a readable body.
What it means
Thrown by the shared streaming-transcription helper when the upload POST returned 2xx but `response.body` is null, so there is no SSE stream to pipe through `createSSETransformer()`. The response must be a streaming body — a body-less success means the transport contract for server-sent events is broken before any transcript delta can arrive.
Solutions
- Confirm the backend really streams SSE for this endpoint (curl -N and watch for events).
- Bypass or reconfigure proxies/CDNs for the transcription route (disable buffering, preserve transfer-encoding).
- In tests, make fetch stubs return a real `ReadableStream` body.
- If the environment cannot stream, use a buffered (non-streaming) transcription call instead of this helper.
Example fix
// before
if (!response.body)
throw new Error('Streaming transcription response is missing a readable body.')
// after — combine with the status check and name the likely culprit
if (!response.ok || !response.body)
throw new Error(`Streaming transcription unusable response: status=${response.status} body=${response.body ? 'present' : 'missing — SSE stripped by proxy/runtime?'}`) Defensive patterns
Strategy: type-guard
Validate before calling
// pre-flight the runtime before first use
if (typeof ReadableStream === 'undefined')
throw new Error('This runtime cannot consume SSE transcription — use buffered mode') Type guard
function isStreamingResponse(res: Response): res is Response & { body: ReadableStream<Uint8Array> } {
return res.ok && res.body instanceof ReadableStream
} Try / catch
if (!isStreamingResponse(response)) // degrade to a buffered transcription call rather than piping return bufferedTranscribe(audioStream)
Prevention
- Ensure the runtime's fetch exposes response streams.
- Keep SSE routes excluded from buffering proxies/CDNs.
- Mock fetch in tests with real ReadableStream bodies.
- Offer a buffered transcription fallback for restricted environments.
When it happens
Trigger: Intermediary (proxy, service worker, antivirus TLS inspection) converting the streamed response into a buffered, body-less one; runtime fetch lacking ReadableStream response support; server answering 204/HEAD-like empty responses on success; test environment mocking fetch without bodies.
Common situations: Corporate proxy stripping SSE; happy-path unit test with a stubbed fetch returning `{ ok: true, body: undefined }`; Electron main-process fetch shim without stream plumbing; misconfigured CDN buffering `/transcriptions` endpoints.
Related errors
- Streaming transcription request failed with status
- OpenRouter audio response has no body
- Aliyun NLS returned a non-streaming result unexpectedly.
- [Hearing Pipeline] Stream input not supported
- MiMo transcription failed
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/460ab8dc288af50b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/stage-ui/src/libs/providers/stream-transcription/index.ts:157
const requestTarget = options.baseURL instanceof URL
? options.baseURL
: new URL(typeof options.baseURL === 'string' ? options.baseURL : 'http://localhost')
const response = await fetcher(requestTarget, {
body: audioStream,
// Browser fetch requires half-duplex mode for a ReadableStream body.
// Keep this at the transport boundary so every SSE transcription
// provider receives the required request option.
duplex: 'half',
headers: options.headers,
method: 'POST',
signal: options.abortSignal,
} as RequestInit & { duplex: 'half' })
if (!response.ok)
throw new Error(`Streaming transcription request failed with status ${response.status}`)
if (!response.body)
throw new Error('Streaming transcription response is missing a readable body.')
await response.body
.pipeThrough(createSSETransformer())
.pipeTo(new WritableStream<AIRIStreamTranscriptionDelta>({
write: (chunk) => {
fullStreamCtrl?.enqueue(chunk)
if (chunk.type === 'transcript.text.delta') {
text += chunk.delta
textStreamCtrl?.enqueue(chunk.delta)
}
else if (chunk.type === 'transcript.text.snapshot') {
text = chunk.text
}
},
close: () => {
fullStreamCtrl?.close()
textStreamCtrl?.close()
},View on GitHub (pinned to 677329427f)