Khi Nào Nên Sử Dụng `multipart/form-data` và `application/x-www-form-urlencoded`?

Quentin đã đưa ra câu trả lời chính xác: sử dụng multipart/form-data khi biểu mẫu chứa các tệp tải lên và sử dụng application/x-www-form-urlencoded cho các trường hợp còn lại. Đây là định dạng mặc định nếu bạn bỏ qua thuộc tính enctype. Bài viết này sẽ đi sâu hơn vào lý do tại sao điều này đúng, kèm theo các ví dụ minh họa cụ thể.

I. Tham Khảo Tiêu Chuẩn HTML5

HTML5 định nghĩa ba giá trị có thể cho thuộc tính enctype của thẻ <form>:

  • application/x-www-form-urlencoded
  • multipart/form-data (chi tiết kỹ thuật được mô tả trong RFC7578)
  • text/plain (ít an toàn và không nên sử dụng trong môi trường thực tế)

Chúng ta sẽ tập trung vào hai loại đầu tiên để hiểu rõ hơn về cách chúng hoạt động.

II. Ví Dụ Thực Tế

Để hiểu rõ hơn sự khác biệt, hãy xem xét một ví dụ đơn giản về cách mỗi phương thức này hoạt động.

Chúng ta sẽ sử dụng các công cụ sau:

  • nc -l hoặc một máy chủ ECHO đơn giản để lắng nghe các yêu cầu HTTP.
  • Trình duyệt web (ví dụ: Firefox, Chrome) hoặc cURL để gửi biểu mẫu.
  1. Tạo Biểu Mẫu HTML:

    Lưu đoạn mã sau vào một tệp .html (ví dụ: upload.html):

    <!DOCTYPE html>
    <html>
    <head>
      <title>Upload Form</title>
    </head>
    <body>
      <form action="http://localhost:8000" method="post" enctype="multipart/form-data">
        Text 1: <input type="text" name="text1" value="text default"><br>
        Text 2: <input type="text" name="text2" value="aωb"><br>
        File 1: <input type="file" name="file1"><br>
        File 2: <input type="file" name="file2"><br>
        File 3: <input type="file" name="file3"><br>
        <input type="submit" value="Submit">
      </form>
    </body>
    </html>

    Trong ví dụ này, chúng ta thiết lập giá trị mặc định cho trường text2aωb, trong đó ω là ký tự Omega (U+03C9), được biểu diễn bằng các byte 61 CF 89 62 trong UTF-8.

  2. Tạo Các Tệp Để Tải Lên:

    Tạo ba tệp với nội dung khác nhau:

    echo 'Content of a.txt.' > a.txt
    echo '<p>Content of a.html.</p>' > a.html
    printf 'axCFx89b' > binary

    Tệp binary chứa 4 byte: a, 0xCF, 0x89b.

  3. Khởi Động Máy Chủ Echo:

    Sử dụng lệnh sau để tạo một máy chủ echo đơn giản lắng nghe trên cổng 8000:

    while true; do printf "" | nc -l 8000 localhost; done
  4. Thử Nghiệm:

    Mở tệp upload.html trong trình duyệt, chọn các tệp bạn vừa tạo và nhấn nút “Submit”. Kiểm tra đầu ra trên thiết bị đầu cuối nơi bạn đang chạy nc.

    nc sẽ in ra yêu cầu HTTP đã nhận được.

III. Phân Tích multipart/form-data

Khi sử dụng multipart/form-data, trình duyệt (trong trường hợp này là Firefox) sẽ gửi một yêu cầu tương tự như sau:

POST / HTTP/1.1
... (các header khác) ...
Content-Type: multipart/form-data; boundary=-735323031399963166993862150
Content-Length: 834

--735323031399963166993862150
Content-Disposition: form-data; name="text1"

text default
--735323031399963166993862150
Content-Disposition: form-data; name="text2"

aωb
--735323031399963166993862150
Content-Disposition: form-data; name="file1"; filename="a.txt"
Content-Type: text/plain

Content of a.txt.
--735323031399963166993862150
Content-Disposition: form-data; name="file2"; filename="a.html"
Content-Type: text/html

<p>Content of a.html.</p>
--735323031399963166993862150
Content-Disposition: form-data; name="file3"; filename="binary"
Content-Type: application/octet-stream

aωb
--735323031399963166993862150--

Phân tích chi tiết:

  • Content-Type: multipart/form-data; boundary=-735323031399963166993862150: Xác định loại nội dung là multipart/form-data và chỉ định chuỗi boundary dùng để phân tách các trường. Lưu ý rằng chuỗi boundary thực tế bắt đầu bằng hai dấu gạch ngang (--). RFC 7578 quy định điều này:

    “Các phần được phân tách bằng dấu phân cách ranh giới, được xây dựng bằng CRLF, “-” và giá trị của tham số “ranh giới”.”

  • Mỗi trường có các header riêng: Content-Disposition: form-data;, name của trường, filename (nếu là tệp) và Content-Type tương ứng.

  • Dữ liệu được gửi nguyên trạng cho cả trường văn bản và tệp nhị phân. Ví dụ, chuỗi aωb (trong UTF-8) được gửi dưới dạng các byte 61 CF 89 62.

Trình duyệt phải chọn một chuỗi boundary duy nhất, không xuất hiện trong bất kỳ dữ liệu nào của các trường. Điều này đảm bảo việc phân tách chính xác các phần dữ liệu.

IV. Phân Tích application/x-www-form-urlencoded

Bây giờ, hãy thay đổi enctype thành application/x-www-form-urlencoded, tải lại trang và gửi lại biểu mẫu.

<form action="http://localhost:8000" method="post" enctype="application/x-www-form-urlencoded">

Trình duyệt sẽ gửi một yêu cầu như sau:

POST / HTTP/1.1
... (các header khác) ...
Content-Type: application/x-www-form-urlencoded
Content-Length: 51

text1=text+default&text2=a%CF%89b&file1=a.txt&file2=a.html&file3=binary

Trong trường hợp này:

  • Dữ liệu của các tệp không được gửi, chỉ có tên tệp.
  • Các ký tự đặc biệt và khoảng trắng được mã hóa: khoảng trắng thành +, các ký tự không in được thành %xx (trong đó xx là mã hex của ký tự). Ví dụ, ω được mã hóa thành %CF%89, chiếm 3 byte.

V. So Sánh và Kết Luận

Từ các ví dụ trên, chúng ta có thể rút ra những kết luận sau:

  • multipart/form-data: Thích hợp cho việc tải lên tệp, vì nó cho phép gửi dữ liệu nhị phân mà không cần mã hóa. Có thêm chi phí cho chuỗi boundary, nhưng mỗi byte dữ liệu được gửi bằng một byte.

  • application/x-www-form-urlencoded: Không phù hợp cho việc tải lên tệp, vì nó chỉ gửi tên tệp và mã hóa các ký tự không in được. Điều này dẫn đến tăng kích thước dữ liệu đáng kể (mỗi ký tự không in được chiếm 3 byte).

Tóm lại:

  • Sử dụng multipart/form-data khi biểu mẫu chứa các tệp cần tải lên.
  • Sử dụng application/x-www-form-urlencoded cho các biểu mẫu chỉ chứa dữ liệu văn bản đơn giản.

Ngay cả khi có thể gửi tệp bằng application/x-www-form-urlencoded, nó sẽ rất kém hiệu quả do việc mã hóa dữ liệu. Đối với các trường văn bản chứa chủ yếu các ký tự in được, sự khác biệt về hiệu quả không đáng kể, do đó application/x-www-form-urlencoded vẫn là một lựa chọn hợp lý trong trường hợp này.