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:

Terminal
$ 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
    }
  ]
}
POST/api/echo
title=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.

upload.mjs
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);
Output (Node 24)
{ 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.

upload.py
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"])
Output
{'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.

Program.cs
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"));
Output (.NET 10)
{"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 kodeLibraryYang diterima server
Mengisi header Content-Type secara manual dengan multipart/form-datafetch, requests, HttpClient (menimpa Content-Type formulir)Tidak ada: Missing content-type boundary. Boundary yang memisahkan bagian-bagiannya hilang
Tidak menyertakan nama fileHttpClient (form.Add(content, name))Isi file sebagai field teks, dan tidak ada file
Tidak menyertakan nama filefetch (Blob tanpa nama)File bernama blob
Tidak menyertakan content typecurl, requests (objek file saja), HttpClientcurl: application/octet-stream; requests dan HttpClient: content type kosong
Memakai data= tanpa files=requestsSama sekali bukan multipart: application/x-www-form-urlencoded

Kesalahan boundary adalah yang paling umum. Inilah yang dilihat server:

JSON
{
  "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.

UploadTests.cs
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.

Terminal
$ 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"}]}
Contoh ini memakai endpoint untuk anggota yang sudah masuk.MasukBuat akun gratis

Yang perlu diperiksa di kode unggah Anda

  • Header Content-Type diserahkan ke library, agar membawa boundary.
  • Setiap bagian file punya nama file dan content type.
  • Nama field cocok dengan yang diharapkan server (file, files, atau upload).
  • 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.