remix: Cannot cancel a stream that already has a reader
What version of Remix are you using?
1.6.7
Steps to Reproduce
I’m executing a http request from a route Action but I’m getting this error because Server is busy (not responding):
[frontend] /frontend/node_modules/web-streams-polyfill/src/lib/readable-stream.ts:149
[frontend] return promiseRejectedWith(new TypeError('Cannot cancel a stream that already has a reader'));
[frontend] ^
[frontend] TypeError: Cannot cancel a stream that already has a reader
[frontend] at ReadableStream.cancel (/frontend/node_modules/web-streams-polyfill/src/lib/readable-stream.ts:149:34)
[frontend] at abort (/frontend/node_modules/@remix-run/web-fetch/src/fetch.js:75:18)
[frontend] at AbortSignal.abortAndFinalize (/frontend/node_modules/@remix-run/web-fetch/src/fetch.js:91:4)
[frontend] at AbortSignal.[nodejs.internal.kHybridDispatch] (node:internal/event_target:643:20)
[frontend] at AbortSignal.dispatchEvent (node:internal/event_target:585:26)
[frontend] at abortSignal (node:internal/abort_controller:284:10)
[frontend] at AbortController.abort (node:internal/abort_controller:315:5)
[frontend] at Timeout._onTimeout (/frontend/apps/webapp/app/utils/http.ts:16:42)
[frontend] at listOnTimeout (node:internal/timers:559:17)
[frontend] at processTimers (node:internal/timers:502:7)
I created a simple utility to support timeout with fetch:
const DEFAULT_TIMEOUT = 5000;
export type HttpOptions = RequestInit & {
timeout?: number;
defaultResponse?: unknown;
parseJSON?: boolean;
}
async function httpRequest(
resource: string,
options: HttpOptions = { timeout: DEFAULT_TIMEOUT, parseJSON: true },
) {
const { timeout, ...rest } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout || DEFAULT_TIMEOUT);
try {
const response = await fetch(resource, {
...rest,
signal: controller.signal
});
clearTimeout(id);
return rest.parseJSON === false ? response : response.json();
} catch (error) {
if (rest.defaultResponse !== undefined) {
return rest.defaultResponse;
}
if (controller.signal.aborted) {
throw new Error('Server is busy, please try again');
}
throw error;
}
}
export { httpRequest };
Then I’m using that method to execute a POST request:
export const action: ActionFunction = async ({ request }) => {
const formData = await request.formData();
const email = formData.get('email');
try {
await httpRequest(`${apiURL}/auth`, {
method: 'POST',
body: JSON.stringify({
email,
}),
parseJSON: false,
});
// ...
} catch (error) {
return {
error: (error as Error)?.message,
}
}
};
Also I’m already using CatchBoundary and ErrorBoundary for this page:
function renderLoginPage(error?: Error) {
const data = useLoaderData();
return (
<div id="main-content" className="relative pb-36">
<LoginPage {...data} error={error} />
</div>
);
}
export const CatchBoundary = () => {
const caught = useCatch();
return renderLoginPage(caught.data);
};
export const ErrorBoundary: ErrorBoundaryComponent = ({ error }) => {
return renderLoginPage(error);
};
export default function () {
return renderLoginPage();
}
Expected Behavior
Be able to get the error message using useActionData Hook from Remix, it’s working for other validations before executing the HTTP request.
Actual Behavior
Getting an unexpected exception:

Thanks for your help! ❤️
About this issue
- Original URL
- State: open
- Created 2 years ago
- Reactions: 18
- Comments: 20 (5 by maintainers)
@MisteryPoints Try cloning the request object, and read data separately from that cloned object. Even I faced the same issue, and have solved it by following this approach -
I see this was assigned and then unassigned - any progress on this?
Also, attach a repository for reproduce this issue
https://github.com/kayac-chang/remix-abort-test/tree/main
https://gitpod.io/start/#kayacchang-remixabortte-0ohsnxppt54
This still seems to be an active issue, I just ran into it too.
i found this issue at fetch writeToStream(request_, request); https://github.com/remix-run/web-std-io/blob/main/packages/fetch/src/fetch.js#L317C3-L317C16 bodey.js StreamIterableIterator.getReader not rleaseLock or cancel who can fix it ??
It’s working great from the loader of the routes with RemixJS 1.7.0, but I’m getting this error with a POST HTTP request from a simple action:
Output:
Please let me know and thanks for your help!
I have similar issue, but not the same. “TypeError: This stream has already been locked for exclusive reading by another reader”
Im trying to request text from Form Data and Files too. But Form has 2 different methods, but not possible to read the request with both of them. Sorry if my code isnt efficent enough but im trying.
Also facing this issue, the following is my investigation
I think this issue can’t be solve by clone the request, it is when request aborted will throw an error which even can’t be catch from user side, so the server crash by this unhandled error.
remix-run/nodeoverwrite the default fetch under the hood, https://github.com/remix-run/remix/blob/27e7ac2959146e8c158382da2a65f9afe9d1a67f/packages/remix-node/globals.ts#L55the implementation in
remix-run/web-std-iocould be found athttps://github.com/remix-run/web-std-io/blob/d2a003fe92096aaf97ab2a618b74875ccaadc280/packages/fetch/src/fetch.js#L39
the issue is https://github.com/remix-run/web-std-io/blob/d2a003fe92096aaf97ab2a618b74875ccaadc280/packages/fetch/src/fetch.js#L75
this error is throw by underline polyfill
readable stream, seems like it will throw error if the response body has been locked (because it will be consume by http request).https://github.com/MattiasBuelens/web-streams-polyfill/blob/d354a7457ca8a24030dbd0a135ee40baed7c774d/src/lib/readable-stream.ts#L149
seems like the test case didn’t do a good job, the controller abort the request before it actually fired, the request.body is not be consume by reader yet. https://github.com/remix-run/web-std-io/blob/d2a003fe92096aaf97ab2a618b74875ccaadc280/packages/fetch/test/main.js#L1129
the test case below try to defer the controller abort, after the request.body be consume.
I didn’t solve this issue, need other people input.
I see this error when canceling a fetch request with
AbortController. When I use remix’s fetch library withgraphql-requestinstead of the defaultnode-fetchthat it uses, I run into this sometimes when callingcontroller.abort()For now, I decided not to use Remix’s custom fetch library for that purpose.
The Remix team has been working on that; they want to remove the polyfill on node> = 18. but the
readable streamis still experimental so it may take some time. https://github.com/remix-run/web-std-io/pull/42Thank the team for the fantastic work here! 🙏🙏🙏
when the fetch request times out, abort will be triggered and an error will be throw
Thank you for the suggestion, I will do that as soon as possible.
I also ran into this as well. For now, as a workaround (to @aniravi24’s point of avoiding the web-fetch usage), I did the following:
setupFetch()somewhere common (or in root)