{"id":"5a79dabdcf68b4c1","repo":"sindresorhus/got","slug":"the-reassigned-stream-body-must-be-readable-ensur","errorCode":null,"errorMessage":"The reassigned stream body must be readable. Ensure you provide a fresh, readable stream in the beforeRetry hook.","messagePattern":"The reassigned stream body must be readable\\. Ensure you provide a fresh, readable stream in the beforeRetry hook\\.","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"source/core/index.ts","lineNumber":631,"sourceCode":"\t\t\t\t\t// 2. If body was reassigned, we MUST destroy the OLD stream to prevent memory leaks\n\t\t\t\t\t// 3. We must restore the body reference after destroy() for identity checks in promise wrapper\n\t\t\t\t\t// 4. We cannot use the normal setter after destroy() because it validates stream readability\n\t\t\t\t\ttry {\n\t\t\t\t\t\tif (bodyWasReassigned) {\n\t\t\t\t\t\t\tconst oldBody = bodyBeforeHooks;\n\t\t\t\t\t\t\t// Temporarily clear body to prevent destroy() from destroying the new stream\n\t\t\t\t\t\t\tthis.options.body = undefined;\n\t\t\t\t\t\t\tthis.destroy();\n\n\t\t\t\t\t\t\t// Clean up the old stream resource if it's a stream and different from new body\n\t\t\t\t\t\t\t// (edge case: if old and new are same stream object, don't destroy it)\n\t\t\t\t\t\t\tif (is.nodeStream(oldBody) && oldBody !== bodyAfterHooks) {\n\t\t\t\t\t\t\t\toldBody.destroy();\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// Restore new body for promise wrapper's identity check\n\t\t\t\t\t\t\tif (is.nodeStream(bodyAfterHooks) && (bodyAfterHooks.readableEnded || bodyAfterHooks.destroyed)) {\n\t\t\t\t\t\t\t\tthrow new TypeError('The reassigned stream body must be readable. Ensure you provide a fresh, readable stream in the beforeRetry hook.');\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tthis.options.body = bodyAfterHooks;\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Body wasn't reassigned - use normal destroy flow which handles body cleanup\n\t\t\t\t\t\t\tthis.destroy();\n\t\t\t\t\t\t\t// Note: We do NOT restore the body reference here. The stream was destroyed by _destroy()\n\t\t\t\t\t\t\t// and should not be accessed. The promise wrapper will see that body identity hasn't changed\n\t\t\t\t\t\t\t// and will detect it's a consumed stream, which is the correct behavior.\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (error_: unknown) {\n\t\t\t\t\t\tconst normalizedError = normalizeError(error_);\n\t\t\t\t\t\tvoid this._error(new RequestError(normalizedError.message, normalizedError, this));\n\t\t\t\t\t\treturn;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Publish retry event\n\t\t\t\t\tpublishRetry({","sourceCodeStart":613,"sourceCodeEnd":649,"githubUrl":"https://github.com/sindresorhus/got/blob/e3924aa1e53a6ca3eb93a43618ce532442a89b40/source/core/index.ts#L613-L649","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before — same stream reused, consumed on retry #1\nconst stream = fs.createReadStream(file);\nhooks: { beforeRetry: [(options) => { options.body = stream; }] }\n\n// after — fresh stream on every retry\nhooks: {\n  beforeRetry: [(options) => { options.body = fs.createReadStream(file); }]\n}","handlingStrategy":"validation","validationCode":"import {isReadable} from 'node:stream';\n\nfunction assertStreamReadableForRetry(stream) {\n  if (stream && typeof stream.readableEnded === 'boolean' && stream.readableEnded) {\n    throw new TypeError('beforeRetry body stream is already ended — provide a fresh stream');\n  }\n  if (stream && stream.destroyed) {\n    throw new TypeError('beforeRetry body stream is destroyed — provide a fresh stream');\n  }\n}\n\n// inside your beforeRetry hook:\nhooks: {\n  beforeRetry: [(options) => {\n    const fresh = fs.createReadStream(path);\n    assertStreamReadableForRetry(fresh);\n    options.body = fresh;\n  }]\n}","typeGuard":"import {Readable} from 'node:stream';\n\nfunction isFreshReadableStream(v: unknown): v is Readable {\n  return v instanceof Readable && !v.destroyed && !v.readableEnded;\n}","tryCatchPattern":"try {\n  await got(url, { body: stream, retry: { limit: 3 }, hooks: { beforeRetry: [reStream] } });\n} catch (error) {\n  if (error instanceof TypeError && /reassigned stream body must be readable/.test(error.message)) {\n    throw new Error('beforeRetry hook returned an exhausted stream — regenerate the stream per retry', { cause: error });\n  }\n  throw error;\n}","preventionTips":["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."],"tags":["retry","hooks","stream-body","before-retry","resource-leak"],"analyzedSha":"e3924aa1e53a6ca3eb93a43618ce532442a89b40","analyzedAt":"2026-08-03T19:22:24.770Z","schemaVersion":2}