-
Notifications
You must be signed in to change notification settings - Fork 539
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
An example of streaming form data using http post request #2202
Comments
@Raynos this is about |
Scanning through the code, the current |
If using I'm thinking something like this: new ReadableStream({
async pull(controller) {
for await (let chunk of stream) {
controller.enqueue(chunk)
};
},
});
} Haven't tested, but should be doable |
A part from that, maybe we might want to document & test how to do it. |
Using Streaming should be supported by I am not sure if
One of the easiest way is use const FormData = require('form-data')
const fs = require('fs')
const { fetch } = require('undici')
const form = new FormData();
form.append('field', 'foo');
form.append('file', fs.createReadStream('./index.js'));
const readable = new ReadableStream({
async pull(controller) {
return new Promise(function(resolve) {
form.on('data', function(chunk) {
controller.enqueue(chunk)
})
form.once('end', function() {
resolve()
})
form.resume()
})
}
})
fetch('http://localhost:3000/form-data', {
method: 'POST',
headers: form.getHeaders(),
body: readable,
duplex: 'half'
})
.then((res) => res.text())
.then(console.log)
|
Not an expert on the spec, but going through it, it seems it aims to be lazy-enqueued, waiting until some implementation disturbs the stream. Although I'm sure that Beyond that, I believe this is currently possibly without relying on the experimental |
So I ran into this issue with I did not think of wrapping it in ReadableStream. Either undici or request would help a lot. We eventually used https://stackoverflow.com/a/75795888 which is an undocumented work around ... For example mikeal request jjust documents how to use formdata in the readme and its great ( https://github.com/request/request#multipartform-data-multipart-form-uploads ) |
SEperately I find the undici README really frustrating. Look at mikeal request ( https://github.com/request/request#requestoptions-callback ) I see everything in one easy to read human friendly place. https://github.com/nodejs/undici#undicirequesturl-options-promise With undici I see nothing, and have to click
|
The buffering in memory is why I generally recommend not using FormData (& .formData()) in node...
AFAICT the file-backed Blob implementation only takes effect when specifically creating a blob via
I think this should work: import { openAsBlob } from 'node:fs'
const blob = await openAsBlob('README.md')
const fd = new FormData()
fd.set('file', blob) of course at some point it'll still be buffered in memory or this const blob1 = await openAsBlob('...')
const blob2 = await openAsBlob('...')
const file = new File([blob1, blob2], 'file-amalgamate.txt') |
Good catch. Maybe some tricks can be done here as how it is applied to
Sure! I was mostly wondering if maybe browsers do it in this way |
I think there are two takes in this feedback:
As both provide a subpar experience in this case. (This shows that most of the maintaienrs of this seldomly send multipart requests from a server). |
I am uploading audio files to the ACR audible fingerprinting external HTTP API. I wish I didn't need to do formdata either, we looked at just taking their curl example from their documentation and spawning it as a child process because at least it doesnt buffer the entire audio file into memory ... |
The other off hand comment about the confusion I have with the README is its own issue now #2208 |
The workaround I found exploits ( Lines 1475 to 1480 in 59abe3f
const formData = new FormData()
formData.append('user_id', 42)
formData.set('audio', {
[Symbol.toStringTag]: 'File',
name: 'music.mp3',
stream: () => createReadStream('./music.mp3'),
})
const response = await fetch('http://api.example.com', {
method: 'POST',
body: formData,
}) The fact that the web blob api happens to return a stream, to stream over the in memory bytes allows us to stream over file IO or network IO instead. |
does this example not work (haven't tried it out myself)? import { openAsBlob } from 'node:fs'
const formData = new FormData()
formData.append('user_id', 42)
formData.set('audio', await openAsBlob('./music.mp3'))
const response = await fetch('http://api.example.com', {
method: 'POST',
body: formData,
}) |
Yes, it can runs normally. Edit: checked by the memory usage, and a big file. It is actually streaming the data. import { createServer } from 'http'
import { openAsBlob } from 'node:fs'
const http = createServer(function(req, res) {
req.on('data', (chunk) => {
res.write(chunk)
})
req.once('end', () => {
res.end(() => {
http.close()
})
})
}).listen(3000)
const formData = new FormData()
formData.append('field', 42)
formData.set('file', await openAsBlob('./index.mjs'))
const response = await fetch('http://127.0.0.1:3000', {
method: 'POST',
body: formData,
})
console.log(await response.text()) |
The documentation for open as blob does not mention its streaming. Also it's an experimental method, also it claims to do Ok i tried to read the implementation for this sync fs stat but it goes deep into the C++ , a sync stat in C++ is very different then one in the JS event loop |
Also this experimental method does not exist on nodejs 16 or 18 |
Hi guys, just stumbled upon this issue and wonder how to proceed.
Also, I'm not sure what's the philosophy here, does undici aim to work mostly with WebAPI of node, or it's open for node-specific expansions? |
@KhafraDev wdyt? I'm ok in adding support for Node.js stream in our own In parallel, we should likely add support for |
@mcollina import { createServer } from 'http'
import { openAsBlob } from 'node:fs'
import { text } from 'node:stream/consumers'
import { request } from 'undici'
const http = createServer(function(req, res) {
req.on('data', (chunk) => {
res.write(chunk)
})
req.once('end', () => {
res.end(() => {
http.close()
})
})
}).listen(3000)
const formData = new FormData()
formData.append('field', 42)
formData.set('file', await openAsBlob('./index.mjs'))
// const response = await fetch('http://127.0.0.1:3000', {
// method: 'POST',
// body: formData,
// })
// console.log(await response.text())
const response = await request('http://127.0.0.1:3000', {
method: 'POST',
body: formData
})
console.log(await text(response.body)) Reference: Lines 177 to 187 in 7308f53
|
Then it's just actually something to document. |
But openAsBlob is still experimental and available only in v19+. |
The alternative is adding support for Node.js streams to our FormData. |
|
If there is a shortage of manpower, I can volunteer implementing it. |
The issue is deviating from the spec |
I think we should just document the use of:
|
Alright, so referring to my previous question:
Does undici strictly follow WebAPI spec only? Sorry if it's a wrong thread. |
to the best of our ability |
|
Seems like it works totaly fine even with native import fs from 'node:fs'
const file = await fs.openAsBlob('./big.csv')
const body = new FormData()
body.set('file', file, 'big.csv')
const res = await fetch('http://example.com', {
method: 'POST',
body
}) I would like to mention it in docs. |
go for it! |
Based on climba03003 example from here nodejs#2202 (comment)
This would solve...
I spend two hours with a coworker trying to figure out how to upload a file using multi part formdata encoding and gave up and loaded the entire buffer into memory as a blob using the web api
The implementation should look like...
An example and test demonstrating how to do this without loading the 200mb file into an in memory Buffer.
I have also considered...
Using mikeal/request from 2012.
Additional context
The text was updated successfully, but these errors were encountered: