Closed
Description
Version
v20.12.0
Platform
Linux ljysjk 6.1.43 #1 SMP PREEMPT_DYNAMIC Sun Aug 6 20:05:33 UTC 2023 x86_64 GNU/Linux
Subsystem
No response
What steps will reproduce the bug?
- Open the CodeSandbox (see code below)
- Observe the
ERR_INVALID_ARG_TYPE
error below
CodeSandbox demo: https://codesandbox.io/p/devbox/filehandle-readablewebstream-with-new-response-ljysjk?file=%2Findex.js
import fs from "node:fs/promises";
import http from "node:http";
import path from "node:path";
import { Readable } from "node:stream";
const filePath = "./image.jpg";
const server = http.createServer(async (nodeRequest, nodeResponse) => {
const stats = await fs.stat(filePath);
const fileHandle = await fs.open(filePath);
const webStream = fileHandle.readableWebStream();
nodeResponse.writeHead(200, {
"Content-Disposition": `attachment; filename=${path.basename(filePath)}`,
"Content-Type": "image/jpeg",
"Content-Length": String(stats.size),
});
const nodeStream = Readable.fromWeb(webStream);
nodeStream.pipe(nodeResponse);
});
const port = 3000;
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
Error:
Server running at http://localhost:3000/
node:events:496
throw er; // Unhandled 'error' event
^
TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string or an instance of Buffer or Uint8Array. Received an instance of ArrayBuffer
at readableAddChunkPushByteMode (node:internal/streams/readable:480:28)
at Readable.push (node:internal/streams/readable:390:5)
at node:internal/webstreams/adapters:517:22
Emitted 'error' event on Readable instance at:
at emitErrorNT (node:internal/streams/destroy:169:8)
at emitErrorCloseNT (node:internal/streams/destroy:128:3)
at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
code: 'ERR_INVALID_ARG_TYPE'
}
Node.js v20.12.0
Failed running 'index.js'
Alternative version with new Response()
:
import fs from "node:fs/promises";
import http from "node:http";
import path from "node:path";
const filePath = "./image.jpg";
const server = http.createServer(async (nodeRequest, nodeResponse) => {
const stats = await fs.stat(filePath);
const fileHandle = await fs.open(filePath);
const webStream = fileHandle.readableWebStream();
// Version with new Response()
const webResponse = new Response(webStream, {
status: 200,
headers: new Headers({
"content-disposition": `attachment; filename=${path.basename(filePath)}`,
"content-type": "image/jpeg",
"content-length": String(stats.size),
}),
});
nodeResponse.writeHead(
webResponse.status,
Object.fromEntries(webResponse.headers.entries())
);
webResponse.body.pipeTo(
new WritableStream({
write(chunk) {
nodeResponse.write(chunk);
},
close() {
nodeResponse.end();
},
})
);
});
const port = 3000;
server.listen(port, () => {
console.log(`Server running at http://localhost:${port}/`);
});
How often does it reproduce? Is there a required condition?
Always
What is the expected behavior? Why is that the expected behavior?
Web streams created by fileHandle.readableWebStream()
should be compatible with Readable.fromWeb()
What do you see instead?
Web streams created by fileHandle.readableWebStream()
are incompatible with Readable.fromWeb()
Additional information
Also reported over here:
cc @jasnell
Activity
Ethan-Arrowood commentedon Jul 25, 2024
Taking a look 👀
Ethan-Arrowood commentedon Jul 25, 2024
My understanding is that the
readableWebStream
method returns a ReadableStream inbytes
mode (https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/ReadableStream#type) by default because files can contain any kind of data. As the error suggests,ArrayBuffer
is not directly compatible with Node.js streams.The best solution I could recommend right now is to maybe use a
TransformStream
and convert eachchunk
of theReadableStream
into a Node.js Buffer usingBuffer.from(chunk)
(which should correctly handleArrayBuffer
).Also, I assume the code you provided is just for reproduction purposes, but it's pretty inefficient to go from Node Stream to Web Stream back to Node stream. Keep it all as a Node stream, or convert it to Web stream and keep it as that.
Ethan-Arrowood commentedon Jul 25, 2024
Roughly:
🤷♂️
Ethan-Arrowood commentedon Jul 25, 2024
Regarding an actual fix to Node.js, I'd say we have to improve
Readable.fromWeb
to infer this and maybe do this transform automatically. I don't immediately see an issue with that. WDYT @jasnell ?karlhorky commentedon Jul 25, 2024
Oh ok, interesting - or should there a better default for
file.readableWebStream()
?Right, I can see how converting back and forth multiple times with this code would seem unrealistic or add overhead.
But this may not be so unrealistic if working with a Node.js framework that expects Web streams in user code - eg. Route Handlers in Next.js:
eric-burel/demo-readableWebStream
in @eric-burel's demo repo...where in the background, there may be code like this to convert it to a Node.js stream:
If there was a mode / way of working to instead keep everything as a Web stream, and also serve up Web streams from a Node.js response with
node:http
or similar, then that would I guess be an option for frameworks to migrate to... 🤔Ethan-Arrowood commentedon Jul 25, 2024
Maybe, I'm trying to think through where to best "fix" this. Because both side's limitation make sense to me.
On one hand, you don't really want
file.readableWebStream()
to muck the underlying data. On the other,Readable.fromWeb
should definitely handle this case. So the question comes down to how?jasnell commentedon Jul 26, 2024
FWIW, The
readableWebStream()
does not return abytes
stream by default... If you callconst readable = fileHandle.readableWebStream({ type: 'bytes' })
then you'll get a proper byte-oriented stream that appears to work correctly withnew Response(readable)
... I think maybe the change to make here is thatreadableWebStream()
should return a byte-oriented stream by default.Ethan-Arrowood commentedon Jul 26, 2024
Ah I misunderstood the code here
node/lib/internal/fs/promises.js
Lines 284 to 335 in 2d1b4a8
Do you foresee any issue with that change in default?
jasnell commentedon Jul 26, 2024
I wouldn't imagine any issues but it would need to be a semvver-major change that should be called out in notable changes.
jasnell commentedon Jul 26, 2024
@karlhorky ... can you verify quickly if
const stream = fileHandle.readableWebStream({ type: 'bytes' });
works for your case?karlhorky commentedon Jul 26, 2024
Should be pretty easy to change the CodeSandbox linked above yep.
Although probably I'll need to check that out tomorrow
Ethan-Arrowood commentedon Jul 26, 2024
The method is still labeled as experimental in the docs, so would it really be a major change? I think a fix is appropriate
karlhorky commentedon Jul 27, 2024
Seems to work, yes! 👍
CodeSandbox: https://codesandbox.io/p/devbox/filehandle-readablewebstream-with-new-response-forked-mszlgw?file=%2Findex.js
Changes to original sandbox from PR description:
{ type: 'bytes' }
as an argument tofileHandle.readableWebStream()
"Content-Disposition": `attachment; filename=${path.basename(filePath)}`,
(which also worked to download the file by visiting the page directly, but didn't allow for a very nice demo)fileHandle.close()
, to avoid these warnings:cc @eric-burel
eric-burel commentedon Jul 31, 2024
@karlhorky absolutely wonderful, I'll update my resources on the topic to match this API.
Last tiny issue, I hit some TypeScript issues in the context of Next.js:
When passing the stream to
Response
. But maybe an issue on Next.js side.Note that you can't close a file after the Response is sent in Next.js, so no way to properly close the file (closing it before the stream is sent will trigger an error as expected).
15 remaining items