Bài trước ta thấy một request HTTP chỉ là dòng đầu + các header + dòng trống + body. Giờ ta soi phần header — nơi chứa gần như toàn bộ "trí thông minh" của giao thức. Trong hàng chục header, ba cái quyết định nhiều nhất: Host (lý do một IP phục vụ được nghìn website), Content-Type (client hiểu body là gì), và cặp Content-Length vs Transfer-Encoding: chunked (làm sao biết body dài đến đâu). Hiểu sai cái cuối là nguồn của những response bị treo, bị cắt cụt, thậm chí lỗ hổng bảo mật. Đo thật trong container với curl 7.88 và socket.
Host: một IP, nghìn website
Trước HTTP/1.1, mỗi website cần một IP riêng. Host thay đổi điều đó: nó là header bắt buộc trong HTTP/1.1, cho server biết bạn đang hỏi tên miền nào trong số nhiều tên miền cùng trỏ về một IP (gọi là virtual hosting / name-based hosting). Server đọc Host rồi chọn cấu hình và nội dung tương ứng.
curl -H "Host: blog.vn" http://SERVER/
curl -H "Host: shop.vn" http://SERVER/ # cùng IP, khác nội dung

Hình 1: Ba header cốt lõi — Host cho virtual hosting, Content-Type cho client hiểu body, và hai cách loại trừ nhau để báo độ dài body.
Đo thật với một server phân biệt theo Host: cùng 127.0.0.1:8098, gửi Host: blog.vn trả "Ban dang xem site: blog.vn", gửi Host: shop.vn trả "shop.vn". Chính cơ chế này khiến shared hosting và reverse proxy (bài sau) hoạt động. Cũng vì thế, khi debug DNS chưa trỏ, bạn curl --resolve hay -H "Host:" để test đúng vhost trên IP thật.
Content-Type: nói cho client biết body là gì
Content-Type báo kiểu dữ liệu của body: text/html để trình duyệt render, application/json để client parse JSON, text/plain hiện thô, image/png cho ảnh... Đây là hợp đồng ngữ nghĩa: đặt sai khiến client hiểu nhầm — trả JSON mà gắn text/plain thì trình duyệt hiện chuỗi thô thay vì để JS xử lý; trả HTML mà gắn text/plain thì người dùng thấy nguyên thẻ <h1> thay vì tiêu đề. Với API, sai Content-Type là lỗi tích hợp kinh điển.
Content-Length vs Transfer-Encoding: chunked
Đây là phần tinh tế nhất. Sau khi gửi header và dòng trống, làm sao bên nhận biết body dài đến đâu để đọc đúng, không đọc thiếu (cắt cụt) hay đọc thừa (treo chờ mãi)? HTTP/1.1 có hai cách, và chúng loại trừ nhau:
Content-Length: N: server biết trước độ dài, khai đúngNbyte. Client đọc đủNbyte là xong. Đơn giản, nhưng đòi server phải biết toàn bộ độ dài trước khi gửi.Transfer-Encoding: chunked: server không biết trước độ dài (nội dung sinh dần — streaming, kết quả truy vấn lớn), nên gửi theo từng khúc (chunk). Mỗi khúc mở đầu bằng kích thước của nó viết ở hệ 16, rồi CRLF, rồi dữ liệu, rồi CRLF. Một khúc kích thước0báo hết.

Hình 2: Content-Length: 26 — body đúng 26 byte, đọc xong là hết. chunked — byte thô cho thấy từng khúc 9, D, 4 (hệ 16 = 9, 13, 4 byte) rồi 0 báo hết; curl tự ráp lại thành body hoàn chỉnh.
Byte thô của response chunked cho thấy chính xác cơ chế: 9\r\nPhan mot \r\n (khúc 9 byte), D\r\nroi phan hai \r\n (khúc 0xD = 13 byte), 4\r\nhet.\r\n (4 byte), rồi 0\r\n\r\n báo hết. Client (curl) đọc từng khúc theo kích thước khai báo và tự ráp lại thành Phan mot roi phan hai het. — người dùng không thấy khung chunk. Chunked là cách HTTP/1.1 làm streaming: server bắt đầu gửi trước khi biết tổng độ dài.
Đánh đổi và cạm bẫy
Không bao giờ gửi cả Content-Length lẫn Transfer-Encoding cùng lúc. Chúng loại trừ nhau; gửi cả hai là request/response mập mờ. Đây chính là gốc của lỗ hổng HTTP request smuggling: nếu proxy tin Content-Length còn server backend tin Transfer-Encoding (hay ngược lại), kẻ tấn công nhét được request lậu vào ranh giới giữa hai cách hiểu. RFC quy định Transfer-Encoding thắng, và khi thấy cả hai, nhiều server từ chối request — đúng đắn.
chunked có chi phí khung. Mỗi khúc thêm dòng kích thước + hai CRLF. Với nhiều khúc nhỏ, overhead đáng kể; server thường gộp thành khúc lớn hợp lý. Ngược lại, Content-Length không tốn khung nhưng buộc biết trước độ dài — không hợp cho nội dung sinh dần.
Header không phân biệt hoa thường ở tên. Content-Type, content-type, CONTENT-TYPE là một. Nhưng giá trị thì có thể phân biệt (ví dụ tên file trong Content-Disposition). Đừng dựa vào chữ hoa thường của tên header khi tự parse.
Ba ý mang về
Hostlà lý do một IP phục vụ nghìn website: HTTP/1.1 bắt buộc nó, server chọn nội dung theo tên miền trongHost— đo thật, cùng127.0.0.1:8098màblog.vnvàshop.vncho hai kết quả khác nhau.Content-Typelà hợp đồng ngữ nghĩa của body: sai kiểu khiến client hiểu nhầm (JSON hiện thô, HTML lộ thẻ) — với API đây là lỗi tích hợp phổ biến.- Hai cách báo độ dài body loại trừ nhau:
Content-Length: N(biết trước) hoặcTransfer-Encoding: chunked(streaming, khung<size hệ 16>CRLF<data>CRLF,0báo hết) — đo thật khung chunk9/D/4/0; gửi cả hai là sai giao thức và là gốc của HTTP smuggling.
Nguồn
- MDN — HTTP headers và Transfer-Encoding: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Transfer-Encoding
- RFC 9112 (HTTP/1.1) mục Message Body Length và Chunked Transfer Coding: https://www.rfc-editor.org/rfc/rfc9112#name-transfer-encoding
Phần sau ta xử lý phần dòng đầu response — mã trạng thái: khác biệt thật giữa 301 và 302, giữa 307/308, cách curl -L đi theo redirect, và làm sao một vòng lặp redirect xảy ra.