Menguji unggah file multipart/form-data
Unggah file di curl, JavaScript, Python, dan C#, lihat persis apa yang diterima server, dan hindari kesalahan boundary.
Contoh terakhir dijalankan terhadap API pada
Untuk menguji unggah file, Anda perlu melihat apa yang benar-benar diterima server: field apa, file apa, dengan nama apa, dengan content type dan ukuran berapa. POST /api/echo di https://api.testingapis.com mem-parse request multipart/form-data dan mengirimkannya kembali sebagai JSON. Gratis, tanpa key, tidak menyimpan apa pun, dan menerima request dari origin mana pun, jadi juga bisa dipakai dari browser. Di bawah: unggahan yang berfungsi di curl, JavaScript, Python, dan C#, kesalahan yang merusak unggahan di masing-masing, dan sebuah tes xUnit.
Unggah multipart dengan curl
-F membuat curl mengirim multipart/form-data. name=value adalah field teks, name=@path melampirkan file, dan ;type= menetapkan content type-nya. Tanpa ;type=, curl mengirim application/octet-stream. Response-nya juga berisi detail request lain; di sini jq hanya menyisakan dua bagian yang penting:
$ printf 'id,name\n1,Alice\n2,Bob\n' > report.csv
$ curl -s https://api.testingapis.com/api/echo \
-F "title=Quarterly report" \
-F "file=@report.csv;type=text/csv" | jq '{form, files}'
{
"form": {
"title": "Quarterly report"
},
"files": [
{
"field": "file",
"fileName": "report.csv",
"contentType": "text/csv",
"size": 22
}
]
}/api/echotitle=Quarterly report
JavaScript: FormData dengan fetch
Buat FormData, tambahkan field dan file, lalu berikan sebagai body. Kode yang sama berjalan di browser (dengan file dari <input type="file"> sebagai pengganti readFile) dan di Node 18 atau lebih baru.
import { readFile } from "node:fs/promises";
const form = new FormData();
form.append("title", "Quarterly report");
form.append("file", new Blob([await readFile("report.csv")], { type: "text/csv" }), "report.csv");
// No Content-Type header: fetch adds "multipart/form-data; boundary=..." itself.
const res = await fetch("https://api.testingapis.com/api/echo", { method: "POST", body: form });
const { form: fields, files } = await res.json();
console.log(fields, files);{ title: 'Quarterly report' } [
{
field: 'file',
fileName: 'report.csv',
contentType: 'text/csv',
size: 22
}
]Python: requests
Field teks masuk ke data, file ke files. Berikan setiap file sebuah tuple berisi nama file, objek file, dan content type. Jika hanya memberikan objek file, content type-nya akan hilang.
import requests
with open("report.csv", "rb") as f:
res = requests.post(
"https://api.testingapis.com/api/echo",
data={"title": "Quarterly report"},
files={"file": ("report.csv", f, "text/csv")}, # (file name, file, content type)
)
body = res.json()
print(body["form"], body["files"]){'title': 'Quarterly report'} [{'field': 'file', 'fileName': 'report.csv', 'contentType': 'text/csv', 'size': 22}]C#: MultipartFormDataContent
Tambahkan StringContent untuk setiap field teks dan StreamContent (atau ByteArrayContent) untuk setiap file. Argumen nama file-lah yang menjadikan sebuah bagian sebagai file: jika dihilangkan, byte file tiba sebagai field teks biasa.
using System.Net.Http.Headers;
using System.Text.Json;
using var http = new HttpClient();
using var form = new MultipartFormDataContent();
form.Add(new StringContent("Quarterly report"), "title");
var file = new StreamContent(File.OpenRead("report.csv"));
file.Headers.ContentType = new MediaTypeHeaderValue("text/csv");
form.Add(file, "file", "report.csv"); // the third argument, the file name, makes it a file
using var response = await http.PostAsync("https://api.testingapis.com/api/echo", form);
var echo = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
Console.WriteLine(echo.GetProperty("form"));
Console.WriteLine(echo.GetProperty("files"));{"title":"Quarterly report"}
[{"field":"file","fileName":"report.csv","contentType":"text/csv","size":22}]Kesalahan yang merusak unggahan
Setiap baris dikirim ke /api/echo; kolom terakhir adalah hasil parsing server.
| Yang dilakukan kode | Library | Yang diterima server |
|---|---|---|
Mengisi header Content-Type secara manual dengan multipart/form-data | fetch, requests, HttpClient (menimpa Content-Type formulir) | Tidak ada: Missing content-type boundary. Boundary yang memisahkan bagian-bagiannya hilang |
| Tidak menyertakan nama file | HttpClient (form.Add(content, name)) | Isi file sebagai field teks, dan tidak ada file |
| Tidak menyertakan nama file | fetch (Blob tanpa nama) | File bernama blob |
| Tidak menyertakan content type | curl, requests (objek file saja), HttpClient | curl: application/octet-stream; requests dan HttpClient: content type kosong |
Memakai data= tanpa files= | requests | Sama sekali bukan multipart: application/x-www-form-urlencoded |
Kesalahan boundary adalah yang paling umum. Inilah yang dilihat server:
{
"form": { "parseError": "Missing content-type boundary." },
"files": null,
"contentType": "multipart/form-data"
}Tes xUnit untuk unggahan
Tes ini memeriksa apa yang sampai ke server, bukan hanya bahwa request mengembalikan 200. Lulus dengan xUnit 2.9 di .NET 10.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Xunit;
public class UploadTests
{
[Fact]
public async Task The_file_arrives_with_its_name_type_and_size()
{
var bytes = Encoding.UTF8.GetBytes("id,name\n1,Alice\n2,Bob\n");
using var http = new HttpClient();
using var form = new MultipartFormDataContent();
form.Add(new StringContent("Quarterly report"), "title");
var file = new ByteArrayContent(bytes);
file.Headers.ContentType = new MediaTypeHeaderValue("text/csv");
form.Add(file, "file", "report.csv");
using var response = await http.PostAsync("https://api.testingapis.com/api/echo", form);
response.EnsureSuccessStatusCode();
var echo = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement;
Assert.Equal("Quarterly report", echo.GetProperty("form").GetProperty("title").GetString());
var sent = Assert.Single(echo.GetProperty("files").EnumerateArray());
Assert.Equal("file", sent.GetProperty("field").GetString());
Assert.Equal("report.csv", sent.GetProperty("fileName").GetString());
Assert.Equal("text/csv", sent.GetProperty("contentType").GetString());
Assert.Equal(bytes.Length, sent.GetProperty("size").GetInt32());
}
}Checksum dan endpoint unggah yang realistis
Ada dua endpoint lagi untuk anggota yang sudah masuk. POST /api/bodies/formmasuk untuk melihat melaporkan field dan file yang sama, ditambah SHA-256 setiap file, sehingga tes bisa membuktikan byte-nya tiba utuh, bukan hanya ukurannya. POST /api/formdata/uploadmasuk untuk melihat berperilaku seperti API unggah sungguhan: menyimpan file dan menjawab 201 Created dengan sebuah id.
$ curl -s https://api.testingapis.com/api/bodies/form -F "file=@report.csv;type=text/csv"
{"contentType":"multipart/form-data; boundary=------------------------8iZt5Fmg2P4QL6fBoNn1Ms","encoding":"multipart","fields":{},"files":[{"field":"file","fileName":"report.csv","contentType":"text/csv","size":22,"sha256":"2e9f445b06e5ca0fb9638798078f6a239ea1fcace10e1dcfb70ceb3a3322ad44"}]}Yang perlu diperiksa di kode unggah Anda
- Header
Content-Typediserahkan ke library, agar membawa boundary. - Setiap bagian file punya nama file dan content type.
- Nama field cocok dengan yang diharapkan server (
file,files, atauupload). - Field teks dan file berada di body multipart yang sama, bukan sebagai request JSON terpisah.
- Tes memeriksa apa yang diterima server: nama, tipe, dan ukuran, bukan hanya kode status.
Untuk bagian request lainnya, termasuk header dan query string, referensi API echo mendaftar setiap field yang dikembalikannya.