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

  1. Pass a fetch implementation that returns real streaming Responses (native fetch / modern undici) via the opts.fetch option.
  2. Wrap your fetch to repair null bodies: read arrayBuffer() and return new Response(buf, ...) so .body is a ReadableStream.
  3. Check response.status/body before streaming and surface a clear error for 204/304 instead of the raw guard.
  4. 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

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


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)