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

  1. 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.
  2. Do not resume/read/pipe the replacement stream before returning from the hook.
  3. 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

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


AI-assisted analysis of sindresorhus/got@e3924aa1e5 (2026-08-03). Data as JSON: /data/errors/5a79dabdcf68b4c1.json. Report an issue: GitHub.