Example: Multipart File Upload with cURL
curl -X POST https://files.example.com/upload \
-H 'Authorization: Bearer ***' \
-F 'file=@report.pdf;type=application/pdf' \
-F 'metadata={"tag":"q3"}'
-Fbuildsmultipart/form-data(binary-safe); do not use-dfor files —--data-binary @fileworks only when the API wants raw bytes, not a form.;type=sets that part's Content-Type. Omit it and curl guesses by extension, which is fine for PDFs and catastrophic for CSVs.- Converted to Node fetch: expect a
FormData()+Blobconstruction, no manual boundary. If a converter emits a hand-builtmultipart/form-datastring with a literal boundary, that's a bug in the converter — ours isn't.
What the server actually receives
Dumping the raw request for the command above against a local echo origin shows why -F exists:
POST /upload HTTP/1.1
Content-Type: multipart/form-data; boundary=------------------------abcdef123456
--------------------------abcdef123456
Content-Disposition: form-data; name="file"; filename="report.pdf"
Content-Type: application/pdf
%PDF-1.4 ...bytes...
--------------------------abcdef123456
Content-Disposition: form-data; name="metadata"
Content-Type: application/json
{"tag":"q3"}
--------------------------abcdef123456--
Three things the transcript proves:
- Each part gets its own
Content-DispositionandContent-Type. The top-levelContent-Typeonly carries the boundary. This is why-Fand-dare not interchangeable:-dproduces one flat body with one content type. metadata={"tag":"q3"}became a part withContent-Type: application/jsonbecause curl recognized the JSON-looking value. That's convenience, not a contract — some APIs expect that part as plain text, and you get to find out in production. Force it:-F 'metadata={"tag":"q3"};type=text/plain'.- The boundary is generated per run. Never hardcode one in converted code; a literal boundary in generated output is a converter bug, not a style choice.
The flags around -F that actually bite
-F 'file=@big.zip'streams from disk; curl does not buffer the whole file. But-F 'note=<contents.txt'reads the file into the part value —<is read-style,@is file-style, and both are silent. A missing file is exit 26, never an empty part.-F 'file=@report.pdf;filename=annual.pdf'renames the part. APIs that validate by filename pattern care; your converted code must set the same filename explicitly.-F 'file=@data.csv;type=text/csv'— for CSV the extension guess istext/csvon modern curl but wasapplication/octet-streamon older builds. If an API rejectsoctet-stream, pin the type.-F 'file=@x.bin;encoding=base64'setsContent-Transfer-Encoding: base64— a header most modern APIs ignore. Don't reach for it unless the API documents it.- No
--fail-with-bodyon an upload means a 413 "file too large" from the edge prints nothing and exits 0. The flags guide covers the assertion shape; uploads are where it matters most because the failure is silent after you paid the egress.
When -F is the wrong flag
--data-binary @report.pdf with -H 'Content-Type: application/pdf' is correct for APIs that take the raw object body (S3 PUT object, some image APIs). The tell: the API docs say "request body is the file", not "multipart form". Sending multipart to a raw-bytes endpoint gives you a body full of boundary lines the server hashes as part of your file — the checksum never matches and no error mentions why.
| API says | flag | body on the wire |
|---|---|---|
"multipart/form-data", form field file | -F 'file=@x' | boundary-delimited parts |
| "request body is the file" | --data-binary @x | raw bytes, one content type |
| "form-urlencoded fields" | -d / --data-urlencode | k=v&k2=v2 |
FAQ
Why is my upload corrupted when sent as raw bytes? -d @file treats content as a form-urlencoded string and will mangle binary. -F (or --form) is the multipart, binary-safe path.
Can I upload without reading the whole file into memory? curl streams from disk either way. In Node, pass a Blob backed by fs.openAsBlob() (or a File) to FormData for the same effect.
What does the converted Go code do for -F? It builds a mime.NewWriter multipart writer — see the curl-to-go route for the generated shape.