diff --git a/.changeset/yellow-hounds-jog.md b/.changeset/yellow-hounds-jog.md new file mode 100644 index 0000000..2b3b40a --- /dev/null +++ b/.changeset/yellow-hounds-jog.md @@ -0,0 +1,5 @@ +--- +'preact-render-to-string': patch +--- + +Only abort/report errors from `renderToPipeableStream()` if the stream hasn't already been closed diff --git a/src/stream-node.js b/src/stream-node.js index bffa20e..e39da31 100644 --- a/src/stream-node.js +++ b/src/stream-node.js @@ -60,7 +60,14 @@ export function renderToPipeableStream(vnode, options, context) { /** * @param {unknown} [reason] */ - abort(reason = new Error('The render was aborted by the server without a reason.')) { + abort( + reason = new Error( + 'The render was aborted by the server without a reason.' + ) + ) { + // Remix/React-Router will always call abort after a timeout, even on success + if (stream.closed) return; + controller.abort(); stream.destroy(); if (options.onError) { diff --git a/test/compat/stream-node.test.js b/test/compat/stream-node.test.js index 5783887..159e0e2 100644 --- a/test/compat/stream-node.test.js +++ b/test/compat/stream-node.test.js @@ -73,4 +73,19 @@ describe('renderToPipeableStream', () => { '' ]); }); + + it('should not error if the stream has already been closed', async () => { + let error; + const sink = createSink(); + const { pipe, abort } = renderToPipeableStream(
bar
, { + onAllReady: () => { + pipe(sink.stream); + }, + onError: (e) => (error = e) + }); + await sink.promise; + abort(); + + expect(error).to.be.undefined; + }); }); diff --git a/test/compat/stream.test.js b/test/compat/stream.test.js index 09f6134..f2378a6 100644 --- a/test/compat/stream.test.js +++ b/test/compat/stream.test.js @@ -82,10 +82,10 @@ describe('renderToReadableStream', () => { const result = await sink.promise; expect(result).to.deep.equal([ - '
loading...
', + '
loading...
', '' ]); });