tursodatabase/turso · error · Error
reader is null
Error message
reader is null
What it means
The HTTP streaming runner (bindings/javascript/sync/packages/common/run.ts:57) reads the response body with response.body?.getReader() and requires a web ReadableStream reader; when response.body is null/undefined it throws 'reader is null' (the surrounding catch then poisons the completion). A null body means the fetch implementation or intermediary returned a Response without a streaming body - the sync protocol requires a stream, so it cannot proceed.
Solutions
- Pass a fetch implementation that returns real streaming Responses (native fetch / modern undici) via the opts.fetch option.
- Wrap your fetch to repair null bodies: read arrayBuffer() and return new Response(buf, ...) so .body is a ReadableStream.
- Check response.status/body before streaming and surface a clear error for 204/304 instead of the raw guard.
- In tests, make the mock return new Response(new ReadableStream({...}) , ...) rather than an empty Response.
Example fix
// before
makeHttpStream({ url, fetch: mockFetch }); // mockFetch returns body-less Responses
// after
const streamingFetch: typeof fetch = async (input, init) => {
const res = await fetch(input, init);
if (res.body == null) {
const buf = await res.arrayBuffer();
return new Response(buf, { status: res.status, headers: res.headers });
}
return res;
};
makeHttpStream({ url, fetch: streamingFetch }); Defensive patterns
Strategy: fallback
Validate before calling
const streamingFetch: typeof fetch = async (input, init) => {
const res = await fetch(input, init);
if (res.body == null) {
const buf = await res.arrayBuffer();
return new Response(buf, { status: res.status, headers: res.headers });
}
return res;
};
// pass as opts.fetch so response.body.getReader() always exists Type guard
const hasStreamingBody = (res: Response): boolean => res.body != null && typeof res.body.getReader === "function";
Try / catch
try {
await sessionOperation();
} catch (e) {
if (e instanceof Error && /reader is null|fetch error/.test(e.message)) {
// poisoned stream: swap in a streaming-capable fetch via opts.fetch and retry
} else throw e;
} Prevention
- Provide a fetch whose Responses have web ReadableStream bodies (native fetch/undici).
- Make test mocks return new Response(readableStream, ...), not empty Responses.
- Watch for 204/304 or proxy responses that legitimately carry no body and handle them upstream.
- Pair with retryFetch for transient 5xx/429 so streams are re-established cleanly.
When it happens
Trigger: Passing a custom opts.fetch (or environment default) whose Response has no body: fetch polyfills/mocks (msw/node older versions, jest mocks) constructing bare Response objects; HTTP 204/304 or gateway responses that legitimately carry no body; proxies or service workers stripping the body; runtimes where Response.body is undefined despite fetch existing.
Common situations: Unit tests with mocked fetch returning new Response() without a stream; corporate proxies or CDN edge errors returning empty-body statuses; Node 16-era undici/polyfill combos where body is a Node stream without getReader; server returning 304 due to conditional headers.
Related errors
- HTTP request failed: . URL: , Method: , Body size: bytes
- cursor response body ended before the cursor response line
- Invalid sync HTTP header
- No cursor response received
- No response body
AI-assisted analysis of tursodatabase/turso@6c72522679 (2026-08-20).
Data as JSON: /api/errors/a22598cbd9ba4660.
Report an issue: GitHub.
Appendix: source
Thrown at bindings/javascript/sync/packages/common/run.ts:57
url = normalizeUrl(url);
try {
let headers = typeof opts.headers === "function" ? await opts.headers() : opts.headers;
if (requestType.headers != null && requestType.headers.length > 0) {
headers = { ...headers };
for (let header of requestType.headers) {
headers[header[0]] = header[1];
}
}
const fetchImpl = opts.fetch ?? fetch;
const response = await fetchImpl(`${url}${requestType.path}`, {
method: requestType.method,
headers: headers,
body: requestType.body ?? null,
});
completion.status(response.status);
const reader = response.body?.getReader();
if (reader == null) {
throw new Error("reader is null");
}
while (true) {
const { done, value } = await reader.read();
if (done) {
completion.done();
break;
}
completion.pushBuffer(value);
}
} catch (error) {
completion.poison(`fetch error: ${error}`);
}
} else if (requestType.type == 'FullRead') {
try {
const metadata = await io.read(requestType.path);
if (metadata != null) {
completion.pushBuffer(metadata);
}View on GitHub (pinned to 6c72522679)