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

Ảnh chụp đoạn mã nền tối giải thích ba header quyết định Host Content-Type và cách báo độ dài body, Host vì sao nhiều site chung một IP vẫn phân biệt được HTTP/1.1 bắt buộc header Host một IP host nhiều domain virtual host server đọc Host để biết bạn hỏi site nào trả nội dung khác nhau curl -H Host blog.vn rồi curl -H Host shop.vn cùng IP khác kết quả, Content-Type client hiểu body là gì text/html trình duyệt render application/json parse JSON text/plain hiện thô sai Content-Type client hiểu nhầm body, hai cách báo body dài bao nhiêu loại trừ nhau Content-Length N biết trước độ dài gửi N byte Transfer-Encoding chunked không biết trước gửi từng khúc khung chunk size hệ 16 CRLF dữ liệu CRLF rồi 0 CRLF CRLF hết dùng cả hai cùng lúc là sai giao thức nguồn của lỗi smuggling

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 đúng N byte. Client đọc đủ N byte 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ước 0 báo hết.

Ảnh chụp bảng kết quả đo thật nền tối chạy curl 7.88 và socket, phần một Host cùng IP 127.0.0.1 8098 khác Host khác nội dung curl -H Host blog.vn trả Ban dang xem site blog.vn curl -H Host shop.vn trả Ban dang xem site shop.vn server chọn theo Host, phần hai Content-Length báo trước 26 byte HTTP/1.1 200 OK Content-Type text/plain Content-Length 26 dòng trống Xin chao tu Content-Length đúng 26 byte client đọc xong là hết, phần ba chunked byte thô thấy khung từng khúc size hệ 16 Transfer-Encoding chunked 9 CRLF Phan mot CRLF khúc 9 byte 0x9 D CRLF roi phan hai CRLF khúc 13 byte 0xD 4 CRLF het CRLF khúc 4 byte 0 CRLF CRLF khúc size 0 là hết curl tự ráp thành Phan mot roi phan hai het

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ề

  1. Host là 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 trong Host — đo thật, cùng 127.0.0.1:8098 mà blog.vn và shop.vn cho hai kết quả khác nhau.
  2. Content-Type là 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.
  3. Hai cách báo độ dài body loại trừ nhau: Content-Length: N (biết trước) hoặc Transfer-Encoding: chunked (streaming, khung <size hệ 16>CRLF<data>CRLF, 0 báo hết) — đo thật khung chunk 9/D/4/0; gửi cả hai là sai giao thức và là gốc của HTTP smuggling.

Nguồn

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.