sindresorhus/got · error · TypeError
The reassigned stream body must be readable. Ensure you prov
Error message
The reassigned stream body must be readable. Ensure you provide a fresh, readable stream in the beforeRetry hook.
What it means
Thrown at source/core/index.ts:631 during the retry path for body reassignment. When a beforeRetry hook replaces `options.body` with a new stream, got destroys the old (consumed) stream but preserves the new one for the retry attempt. Before restoring it, got checks that the new stream is still readable — if it is already `readableEnded` or `destroyed`, the retry cannot reuse it and this TypeError fires. The check protects against hooks that hand back an already-consumed stream, which would silently produce an empty body on retry.
Source
Thrown at source/core/index.ts:631
// 2. If body was reassigned, we MUST destroy the OLD stream to prevent memory leaks
// 3. We must restore the body reference after destroy() for identity checks in promise wrapper
// 4. We cannot use the normal setter after destroy() because it validates stream readability
try {
if (bodyWasReassigned) {
const oldBody = bodyBeforeHooks;
// Temporarily clear body to prevent destroy() from destroying the new stream
this.options.body = undefined;
this.destroy();
// Clean up the old stream resource if it's a stream and different from new body
// (edge case: if old and new are same stream object, don't destroy it)
if (is.nodeStream(oldBody) && oldBody !== bodyAfterHooks) {
oldBody.destroy();
}
// Restore new body for promise wrapper's identity check
if (is.nodeStream(bodyAfterHooks) && (bodyAfterHooks.readableEnded || bodyAfterHooks.destroyed)) {
throw new TypeError('The reassigned stream body must be readable. Ensure you provide a fresh, readable stream in the beforeRetry hook.');
}
this.options.body = bodyAfterHooks;
} else {
// Body wasn't reassigned - use normal destroy flow which handles body cleanup
this.destroy();
// Note: We do NOT restore the body reference here. The stream was destroyed by _destroy()
// and should not be accessed. The promise wrapper will see that body identity hasn't changed
// and will detect it's a consumed stream, which is the correct behavior.
}
} catch (error_: unknown) {
const normalizedError = normalizeError(error_);
void this._error(new RequestError(normalizedError.message, normalizedError, this));
return;
}
// Publish retry event
publishRetry({View on GitHub (pinned to e3924aa1e5)
Solutions
- In beforeRetry, always create a FRESH stream for each retry — re-run fs.createReadStream(path) or re-acquire the source inside the hook body, not once outside.
- Do not resume/read/pipe the replacement stream before returning from the hook.
- If the source cannot be re-streamed, send a buffer or string body instead of a stream so it can be safely replayed.
Example fix
// before — same stream reused, consumed on retry #1
const stream = fs.createReadStream(file);
hooks: { beforeRetry: [(options) => { options.body = stream; }] }
// after — fresh stream on every retry
hooks: {
beforeRetry: [(options) => { options.body = fs.createReadStream(file); }]
} Defensive patterns
Strategy: validation
Validate before calling
import {isReadable} from 'node:stream';
function assertStreamReadableForRetry(stream) {
if (stream && typeof stream.readableEnded === 'boolean' && stream.readableEnded) {
throw new TypeError('beforeRetry body stream is already ended — provide a fresh stream');
}
if (stream && stream.destroyed) {
throw new TypeError('beforeRetry body stream is destroyed — provide a fresh stream');
}
}
// inside your beforeRetry hook:
hooks: {
beforeRetry: [(options) => {
const fresh = fs.createReadStream(path);
assertStreamReadableForRetry(fresh);
options.body = fresh;
}]
} Type guard
import {Readable} from 'node:stream';
function isFreshReadableStream(v: unknown): v is Readable {
return v instanceof Readable && !v.destroyed && !v.readableEnded;
} Try / catch
try {
await got(url, { body: stream, retry: { limit: 3 }, hooks: { beforeRetry: [reStream] } });
} catch (error) {
if (error instanceof TypeError && /reassigned stream body must be readable/.test(error.message)) {
throw new Error('beforeRetry hook returned an exhausted stream — regenerate the stream per retry', { cause: error });
}
throw error;
} Prevention
- Regenerate the stream inside the beforeRetry hook body (not in a closure outside it) so each retry gets a fresh instance.
- Prefer Buffer/string bodies when the source can be cheaply buffered — they are safely replayable.
- Never pipe, resume, or read the replacement stream before the retry fires.
When it happens
Trigger: A beforeRetry hook sets `options.body = someStream` where someStream has already been read to completion or destroyed; reusing the same stream reference across retries; piping a stream elsewhere before assigning it as the body; manually calling `.read()` or `.resume()` on the replacement stream before the retry fires.
Common situations: Retry logic for streaming uploads where the hook captures a stream variable once (closure) and hands the same instance back on every retry after the first; using a fs.createReadStream that was already consumed by another consumer; hooks that create the stream lazily but cache it incorrectly.
Related errors
- ERR_RETRYING
- The `afterResponse` hook returned an invalid value
- A retry listener has been attached already.
- ECONNRESET
- beforeCache hooks must be synchronous. The hook returned a P
AI-assisted analysis of sindresorhus/got@e3924aa1e5 (2026-08-03).
Data as JSON: /data/errors/5a79dabdcf68b4c1.json.
Report an issue: GitHub.