nodejs / undici

An HTTP/1.1 client, written from scratch for Node.js
https://nodejs.github.io/undici
MIT License
5.92k stars 512 forks source link

How can I use fixed-length multipart request with FormData? #1421

Open ronag opened 2 years ago

ronag commented 2 years ago

Discussed in https://github.com/nodejs/undici/discussions/1420

Originally posted by **nwtgck** May 6, 2022 Hi, I'd like to request a fixed-length multipart request. The code below requested with `transfer-encoding: chunked` without content-length. I assume the length of formData is statically decided. ```js const undici = require("undici"); (async () => { const formData = new undici.FormData(); formData.append("myname", "myvalue"); const res = await undici.request("http://localhost:8181", { body: formData, }); })(); ``` ```console $ nc -lp 8181 PUT / HTTP/1.1 host: localhost:8181 connection: keep-alive content-type: multipart/form-data; boundary=----formdata-undici-0.5140957512643098 transfer-encoding: chunked 64 ------formdata-undici-0.5140957512643098 Content-Disposition: form-data; name="myname" myvalue 2a ------formdata-undici-0.5140957512643098-- 0 ```
ronag commented 2 years ago

As far as I can see there is no reason for this to use chunked-encoding.

nwtgck commented 2 years ago

Thanks for the answer.

thatsmydoing commented 1 year ago

It seems like the issue is that the spec is unclear https://fetch.spec.whatwg.org/#concept-bodyinit-extract

https://github.com/nodejs/undici/blob/8549a9402c46034d6d28b2f6f65d934736e51a59/lib/fetch/body.js#L114-L115

I checked Firefox and it looks like they do set the length in their implementation https://searchfox.org/mozilla-central/rev/8dd35cd8f5284fbaa506aab02fe42fc87efb249e/dom/html/HTMLFormSubmission.cpp#373

ronag commented 1 year ago

@thatsmydoing PR welcome!

KhafraDev commented 1 year ago

This was fixed in f38fbdc0f03d5c19d74d09bc0bee32c58a5afd6f

jimmywarting commented 1 year ago

Just notice something...

here are one other reason of why i prefer the FormData -> Blob conversions more. The formdata has to be immutable see failing test in NodeJS:

const fd = new FormData()

fd.set('foo', 'bar')

const req = new Request('https://httpbin.org/post', {
  method: 'POST',
  body: fd
})

fd.set('foo', 'foo')
// fd.set('foo', 'foooo') could also set the field to a other size making req content-length invalid.

const fd2 = await req.formData()
console.log(fd2.get('foo')) // prints foo in NodeJS and bar in browsers. 

When you convert the formdata to a blob then you will already have strings converted to uint8arrays and they stay immutable. from the blob you also get

  1. a size & type property
  2. consuming the body will be as simple as just yield * blob.stream()
  3. you don't have to duplicate your code as much, clone all entries to a new formData, making another loop just for calculating the size and another loop for the action that consumes the body.
  4. treating the formdata as a blob also means less logic over all.
KhafraDev commented 1 year ago

Would you be willing to make a PR implementing the FormData -> Blob changes?

jimmywarting commented 1 year ago

sure

jimmywarting commented 1 year ago

hmm. couldn't really convert the formdata to a blob as i'm doing in node-fetch and the formdata polyfill for browsers I'm using fetch-blob instead that can accept 3rd party blobs backed up by the fs. And i couldn't really use the FileLike as it did only accept one blob part instead of an array of parts.

https://github.com/nodejs/undici/blob/860c2617ba55d911286f967dbc915f03e89e0d94/lib/fetch/file.js#L95-L97

So it's not spec compatible in that regards...

import { fileFromSync } from 'fetch-blob/from.js'

const fsFile = fileFromSync('file.txt')
const fd = new globalThis.FormData()
fd.set('a', fsFile)
const FileLike = fd.get('a').constructor
new FileLike(['a', 'b']) // don't work...
new FileLike([blob, blob]) // don't work either...
nwtgck commented 1 year ago

undici.fetch() is good but undici.request() still send with transfer-encoding: chunked.

undici.fetch()

const undici = require("undici");

(async () => {
  const formData = new undici.FormData();
  formData.append("myname", "myvalue");
  const res = await undici.fetch("http://localhost:8181", {
    method: "POST",
    body: formData,
  });
})();
$ nc -l 8181
POST / HTTP/1.1
host: localhost:8181
connection: keep-alive
content-type: multipart/form-data; boundary=----formdata-undici-062462082415
accept: */*
accept-language: *
sec-fetch-mode: cors
user-agent: undici
accept-encoding: gzip, deflate
content-length: 130

------formdata-undici-062462082415
Content-Disposition: form-data; name="myname"

myvalue
------formdata-undici-062462082415--

undici.request()

const undici = require("undici");

(async () => {
  const formData = new undici.FormData();
  formData.append("myname", "myvalue");
  const res = await undici.request("http://localhost:8181", {
    body: formData,
  });
})();
$ nc -l 8181
PUT / HTTP/1.1
host: localhost:8181
connection: keep-alive
content-type: multipart/form-data; boundary=----formdata-undici-008681374567
transfer-encoding: chunked

5e
------formdata-undici-008681374567
Content-Disposition: form-data; name="myname"

myvalue

24
------formdata-undici-008681374567--
0
nwtgck commented 1 year ago

Thank you so much for reopening.

wujohns commented 3 months ago

Is the undici has stable version? The file uploading is not work at all in nodejs 20 I upload successful using axios, but the undici always has bug