Danh sách bài viết

Bài 4: HTTP Headers Cần Biết

Bài 4 của series Rust RESTful API — tổng quan 12-15 HTTP header thường gặp nhất trong REST API theo RFC 9110, phân biệt request header và response header, Content-Type với Accept (server gửi gì vs client mong gì), Authorization scheme (Bearer, Basic, custom), Cache-Control + ETag + Vary cho HTTP caching, X-Forwarded-For khi behind proxy, quy ước custom header X- theo RFC 6648, và lock 4 custom header cho Shop API: X-Request-Id, Idempotency-Key, X-RateLimit-*, X-Total-Count.

12/06/2026
10 phút đọc
1 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Biết phân biệt request header và response header, cùng nhóm header dùng cho cả hai chiều.
  • Nắm 12-15 header thường gặp nhất trong REST API theo RFC 9110.
  • Hiểu khác biệt giữa Content-Type (body thật là gì) và Accept (client mong nhận gì).
  • Biết Authorization scheme: Bearer (OAuth/JWT), Basic (legacy), custom scheme (API key).
  • Hiểu cơ chế HTTP caching qua Cache-Control, ETag, Vary, và conditional GET trả 304.
  • Nắm quy ước custom header prefix X- (đã deprecated theo RFC 6648 nhưng vẫn dùng phổ biến).
  • Áp dụng vào Shop API: lock 4 custom header — X-Request-Id, Idempotency-Key, X-RateLimit-*, X-Total-Count.
2

Header Là Gì? Cấu Trúc Key:Value

Một HTTP message gồm ba phần: start line (request line hoặc status line), headers (danh sách metadata), và body tùy chọn. Header là cặp name: value trên từng dòng, kết thúc bằng dòng trống báo hiệu chuyển sang body. RFC 9110 mục 5 chuẩn hóa header thành fields: tên field case-insensitive (Content-Type bằng content-type), nhưng quy ước cộng đồng là Title-Case để dễ đọc.

POST /api/v1/products HTTP/1.1
Host: shop.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
Accept: application/json
User-Agent: ShopMobile/2.1 (iOS 18.2)
X-Request-Id: 7f9c3b1a-5d2e-4a8b-9f1c-2e8d4b6a1c3f

{"name": "Laptop Y", "price": 1499.00}

Một header có thể mang nhiều giá trị qua dấu phẩy: Accept: application/json, text/html. Đây cũng là cách viết gọn cho header lặp nhiều lần với cùng tên. Cú pháp line folding (chia một header qua nhiều dòng bằng khoảng trắng đầu dòng tiếp theo) đã bị RFC 7230 deprecate vào năm 2014 và RFC 9110 cấm sinh ra — chỉ giữ để parser tha cho input cũ. Mỗi header viết trên đúng một dòng.

Giới hạn kích thước header không nằm trong RFC mà do server tự đặt. Nginx mặc định 8 KB cho một header, 16 KB tổng. Vượt giới hạn thường trả 431 Request Header Fields Too Large. Một số CDN có giới hạn riêng và đôi khi cắt header dài âm thầm — chú ý khi truyền cookie hoặc JWT lớn qua nhiều tầng proxy.

3

Request vs Response Header

RFC 9110 phân header theo chiều xuất hiện: chỉ trong request, chỉ trong response, hoặc cả hai. Bảng phân loại nhóm header thông dụng nhất:

Nhóm                Header tiêu biểu
Chỉ request         Accept, Accept-Encoding, Accept-Language, Authorization,
                    Cookie, Host, User-Agent, Referer, Origin, If-None-Match
Chỉ response        Server, Set-Cookie, Location, WWW-Authenticate,
                    ETag, Retry-After, Access-Control-Allow-Origin
Cả hai chiều        Content-Type, Content-Length, Content-Encoding,
                    Cache-Control, Date, Connection, custom X-*

Nhóm chỉ request mô tả thông tin client gửi đi: Host chỉ virtual host đích, Authorization mang credential, Cookie mang state phiên, User-Agent identify client, Referer chỉ trang gốc, Origin dùng cho CORS preflight.

Nhóm chỉ response mô tả thông tin server trả: Server chỉ engine (thường ẩn vì lý do bảo mật), Set-Cookie ra lệnh client lưu cookie, Location chỉ URL mới khi 3xx hoặc resource vừa tạo khi 201, WWW-Authenticate đi cùng 401 báo scheme cần dùng, ETag đánh dấu version representation.

Nhóm cả hai mô tả nội dung message: Content-Type xuất hiện ở body cả request và response, Cache-Control đi hai chiều (client yêu cầu cache behavior, server chỉ định cache policy), custom X-* tự định nghĩa cho cả hai chiều theo nhu cầu.

4

Content-Type vs Accept

Đây là cặp hay nhầm. Quy tắc một câu: Content-Type mô tả body này thật sự là gì; Accept mô tả client đọc được những loại nào.

Content-Type đi với body. Khi client POST JSON, client phải set Content-Type: application/json để server biết cách parse. Khi server trả về JSON, response cũng có Content-Type: application/json để client deserialize đúng. Giá trị là một MIME type theo RFC 6838: application/json, application/xml, text/html, text/plain, application/octet-stream cho binary, multipart/form-data; boundary=... cho upload file.

POST /api/v1/cart/items HTTP/1.1
Content-Type: application/json
Accept: application/json

{"product_id": 43, "quantity": 2}

HTTP/1.1 201 Created
Content-Type: application/json

{"id": 88, "product_id": 43, "quantity": 2}

Accept chỉ xuất hiện trong request và là một danh sách preference với quality value (q=) đánh trọng số từ 0 đến 1. Server đọc danh sách, chọn format khớp nhất nó hỗ trợ và set Content-Type tương ứng cho response. Đây là cơ chế content negotiation — sẽ được mổ kỹ ở Bài 5.

GET /api/v1/products/43 HTTP/1.1
Accept: application/json;q=0.9, application/xml;q=0.5, */*;q=0.1

Shop API mặc định trả application/json cho mọi endpoint. Nếu client gửi Accept: application/xml cho endpoint chỉ hỗ trợ JSON, server trả 406 Not Acceptable. Sau này khi versioning qua header (B30), Shop API sẽ phân biệt version qua vendor MIME — application/vnd.shop.v2+json — đặt trong cùng cơ chế Accept negotiation. Lúc đó client yêu cầu version 2 sẽ gửi Accept: application/vnd.shop.v2+json và server route đúng tới handler v2.

5

Authorization & Cookie — Auth Mechanisms

Header Authorization theo RFC 9110 mục 11 có cú pháp chung Authorization: <scheme> <credentials>. Scheme do IANA register hoặc tự định nghĩa; credential tùy scheme. Ba scheme phổ biến nhất trong REST API:

Bearer theo RFC 6750 là chuẩn cho OAuth 2.0 và JWT. Credential là chuỗi token đã sign sẵn; bất kỳ ai nắm token đều có quyền truy cập (bearer nghĩa là "người mang"), vì vậy chỉ truyền qua HTTPS. Shop API dùng scheme này cho mọi endpoint cần auth.

GET /api/v1/me HTTP/1.1
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIi...

Basic theo RFC 7617 mã hóa username:password bằng Base64 và gửi mỗi request. Đây là scheme legacy — credential gốc đi qua mạng (Base64 không phải encryption), client phải lưu password để gửi lại mỗi lần. Còn dùng cho internal tool, basic auth nginx, healthcheck endpoint không public.

GET /admin HTTP/1.1
Authorization: Basic YWRtaW46c2VjcmV0MTIz

Custom scheme — tự đặt tên (API key, HMAC, AWS Signature v4). Ví dụ Authorization: ApiKey sk_live_4eC39.... Khuyến nghị giữ format chung scheme + space + credentials để tương thích với parser HTTP standard và dễ thay thế sau này.

Cookie là cơ chế khác hẳn: stateful, server lưu phiên trong store (Redis, DB), client chỉ giữ session ID. Server set bằng response header Set-Cookie: SID=abc; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=3600; client tự gửi lại trong request header Cookie: SID=abc cho mọi request cùng domain. Attribute HttpOnly chặn JavaScript đọc cookie (chống XSS), Secure chỉ gửi qua HTTPS, SameSite chống CSRF, Path giới hạn scope URL.

Khi 401 trả về, response phải kèm WWW-Authenticate chỉ scheme client cần dùng. Format: WWW-Authenticate: Bearer realm="api", error="invalid_token". Đây là contract HTTP — không có header này, client SDK chuẩn không biết phải retry kiểu gì.

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="shop-api", error="invalid_token"
Content-Type: application/json

{"error": "Token expired", "code": "TOKEN_EXPIRED", "request_id": "req_8f3c2a"}

Shop API lock Authorization: Bearer <JWT> làm format chuẩn cho mọi endpoint cần auth (chi tiết flow ở G12). Cookie session sẽ chỉ dùng cho admin dashboard nội bộ (B105) — public API thuần token-based để giữ stateless.

6

Cache-Control, ETag, Vary — HTTP Caching

Caching tốt giảm tải server, giảm latency client, tiết kiệm bandwidth. HTTP caching theo RFC 9111 dùng ba header chính: Cache-Control (policy), ETag (version tag), Vary (cache key extension).

Cache-Control chỉ định cache behavior. Các directive thông dụng:

  • public, max-age=3600 — cache layer trung gian (CDN, proxy) được phép cache trong 1 giờ.
  • private, max-age=600 — chỉ browser cache, không cho shared cache (vì response chứa data riêng của user).
  • no-cache — được phép lưu nhưng phải revalidate với server trước mỗi lần dùng. Dùng cho resource thay đổi thường, nhưng tận dụng ETag để tiết kiệm bandwidth.
  • no-storekhông lưu gì hết. Dùng cho data nhạy cảm (banking, profile). Khác hẳn no-cache.
  • must-revalidate — khi response stale, cache phải hỏi server trước khi serve.

ETag là chuỗi tag đánh dấu một version cụ thể của representation. Server tính ETag từ hash content hoặc version number; mỗi lần resource đổi, ETag đổi theo. Client lưu ETag và gửi lại qua If-None-Match trong request kế. Nếu match, server trả 304 Not Modified không kèm body — tiết kiệm toàn bộ payload:

GET /api/v1/products/43 HTTP/1.1
If-None-Match: "v2-a1b2c3d4"

HTTP/1.1 304 Not Modified
ETag: "v2-a1b2c3d4"
Cache-Control: public, max-age=300

Phiên bản cũ hơn là Last-Modified + If-Modified-Since — so theo timestamp. Vẫn dùng được nhưng kém ETag ở độ chính xác (chỉ tính theo giây) và không phân biệt được đổi content cùng giây.

Vary chỉ định cache key gồm những header nào ngoài URL. Ví dụ Vary: Accept-Encoding báo cache rằng response gzip và response raw là hai bản khác nhau, không được serve nhầm. Vary: Authorization báo response phụ thuộc user — quan trọng để CDN không serve nhầm data của user A cho user B.

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: public, max-age=300
ETag: "v2-a1b2c3d4"
Vary: Accept-Encoding, Accept-Language

Shop API sẽ áp dụng caching ở G18: endpoint catalog (GET /api/v1/products, GET /api/v1/products/:slug) trả Cache-Control: public, max-age=300 + ETag + Vary: Accept-Encoding; endpoint user-scoped (GET /api/v1/me, GET /api/v1/cart) trả Cache-Control: private, no-cache + Vary: Authorization; endpoint nhạy cảm (GET /api/v1/orders/:id/payment) trả Cache-Control: no-store.

7

Bảo Mật & Forwarding

Trong production, Shop API gần như luôn đứng sau reverse proxy (nginx, HAProxy) hoặc CDN (Cloudflare, Fastly). Connection thật từ client tới proxy, từ proxy tới app — server app nhìn thấy IP của proxy chứ không phải client. Nhóm header X-Forwarded-* được dùng để proxy chuyển thông tin gốc xuống app:

GET /api/v1/products/43 HTTP/1.1
Host: shop.example.com
X-Forwarded-For: 203.0.113.45, 198.51.100.10
X-Forwarded-Proto: https
X-Forwarded-Host: shop.example.com
X-Real-IP: 203.0.113.45

X-Forwarded-For chứa IP client gốc và chain proxy theo thứ tự — phần tử đầu là client thật, các phần tử sau là proxy trung gian. X-Forwarded-Proto chỉ scheme gốc (https hay http) khi proxy terminate TLS rồi forward HTTP thuần xuống app. RFC 7239 chuẩn hóa lại thành một header thống nhất Forwarded: for=203.0.113.45;proto=https;host=shop.example.com, nhưng X-Forwarded-* vẫn phổ biến hơn vì có trước.

Cảnh báo bảo mật quan trọng: không được trust X-Forwarded-For trực tiếp từ request. Bất kỳ client nào cũng có thể tự set header này. Chỉ trust khi app đứng sau proxy whitelist xác định, và phải lấy IP từ vị trí biết trước trong chain. Trong axum, đây là vai trò middleware (lock chi tiết ở B158) — trust gì, không trust gì là quyết định cấu hình rõ ràng, không được mặc định.

Hai header bảo mật khác cần lock policy:

  • Strict-Transport-Security: max-age=31536000; includeSubDomains; preload — báo browser chỉ dùng HTTPS với domain này trong 1 năm tới. Chống downgrade attack và sslstrip.
  • Content-Security-Policy — danh sách whitelist nguồn JS/CSS/image/connect. Chống XSS bằng cách chặn inline script và resource từ origin lạ.
  • X-Content-Type-Options: nosniff — báo browser không guess content type, dùng đúng Content-Type server gửi. Chống MIME sniffing attack.

CORS dùng nhóm header riêng — Origin (request), Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers (response). Chi tiết cơ chế preflight và policy CORS cho Shop API sẽ là chủ đề riêng ở G16.

8

Custom Header X- Và Convention

Khi cần header riêng cho API, lịch sử dùng prefix X- để phân biệt với header chuẩn IETF: X-Forwarded-For, X-Frame-Options, X-Requested-With. RFC 6648 (năm 2012) chính thức deprecate prefix này. Lý do: một số header bắt đầu là tạm với prefix X- rồi được chấp nhận thành chuẩn, lúc đó phải sống chung cả hai phiên bản (X-Forwarded-ForForwarded) — gây fragmentation. Khuyến nghị mới là đặt tên trực tiếp, không prefix.

Thực tế industry vẫn dùng X- rất nhiều vì hai lý do: giá trị brand recognition (developer thấy X- hiểu ngay là custom), và inertia trong codebase cũ. Nguyên tắc làm việc hiện đại: ưu tiên header standard khi có (Authorization, Content-Type, ETag, Cache-Control); chỉ custom khi không có lựa chọn; tự quyết định có prefix X- hay không miễn nhất quán trong API.

Shop API lock bốn custom header sau cho toàn series:

  • X-Request-Id — UUID v4 do middleware sinh ở edge cho mỗi request. ID này propagate vào mọi log, span tracing, response error envelope, và liên kết trực tiếp với field request_id trong error body envelope đã lock từ Bài 3. Nếu client tự gửi X-Request-Id, server tôn trọng và dùng lại (use case: gateway của client cũng có request_id riêng cần correlate).
  • Idempotency-Key (không prefix X-, theo convention Stripe) — client gửi UUID trong POST để server dedupe khi retry. Nếu nhận lại request cùng key trong cửa sổ 24h, server trả y nguyên response của lần đầu. Dùng cho endpoint POST /api/v1/checkout, POST /api/v1/payments — chi tiết implementation ở G20.
  • X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset — response header báo client biết quota hiện tại. Format: X-RateLimit-Limit: 100 (số request tối đa), X-RateLimit-Remaining: 87 (còn lại), X-RateLimit-Reset: 1717920000 (timestamp Unix khi quota reset). Chi tiết ở G17.
  • X-Total-Count — response header trả tổng số record cho list endpoint phân trang. GET /api/v1/products?page=1&size=20 trả X-Total-Count: 487 trong header bên cạnh array trong body — client tự tính số trang. Dùng cho mọi list endpoint của Shop API (chi tiết ở G7).

Bốn header này được lock vĩnh viễn — đặt vào file shop-common::headers dưới dạng constant để mọi crate dùng cùng tên, không tự ý đổi. Header standard luôn ưu tiên: Authorization cho auth, Content-Type cho body type, ETag cho version tag, Cache-Control cho cache policy, Retry-After cho rate limit retry guidance — không thay thế bằng custom X-.

9

Tổng Kết

  • HTTP header có dạng Name: value, tên field case-insensitive theo RFC 9110 (quy ước Title-Case). Nhiều giá trị tách bằng dấu phẩy; line folding đã deprecated.
  • Request header điển hình: Accept, Authorization, Cookie, User-Agent, Host, Origin, Referer.
  • Response header điển hình: Server, Set-Cookie, Location, WWW-Authenticate, ETag, Retry-After.
  • Header dùng cả hai chiều: Content-Type, Content-Length, Cache-Control, Date, custom X-*.
  • Content-Type = body thật là gì; Accept = client đọc được những loại nào (kèm q= preference cho content negotiation).
  • Authorization: <scheme> <credentials>: Bearer cho OAuth/JWT, Basic legacy, custom cho API key. Shop API lock Authorization: Bearer <JWT> cho mọi endpoint cần auth.
  • Cache-Control: max-age=N giới hạn TTL, no-cache phải revalidate, no-store không lưu gì, private/public phân vùng cache.
  • ETag + If-None-Match → 304 Not Modified, conditional GET tiết kiệm bandwidth.
  • Vary chỉ định cache key gồm header gì ngoài URL; ví dụ Vary: Accept-Encoding, Vary: Authorization.
  • X-Forwarded-For mang IP client thật khi behind proxy; chỉ trust khi proxy nằm trong whitelist.
  • RFC 6648 (2012) deprecate prefix X- cho custom header, nhưng industry vẫn dùng phổ biến. Ưu tiên header standard khi có sẵn.
  • Shop API lock 4 custom header: X-Request-Id (link với request_id trong error envelope), Idempotency-Key (Stripe convention, không prefix), X-RateLimit-Limit/Remaining/Reset, X-Total-Count.
10

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Phân biệt Content-TypeAccept. Khi POST JSON body lên /api/v1/cart/items, client cần set những header gì và giá trị mỗi header là gì?
  2. Bearer token gửi qua header nào và format ra sao? Authorization: Bearer khác Authorization: Basic ở chỗ nào về cơ chế và bảo mật?
  3. Cache-Control: no-cacheCache-Control: no-store khác gì nhau? Nêu một use case Shop API ứng với mỗi directive.
  4. Behind reverse proxy nginx, làm sao server axum lấy được IP client thật? Trust X-Forwarded-For trực tiếp luôn được không? Vì sao?
  5. Shop API lock những custom header gì? Header nào nên dùng standard thay vì X- prefix và header nào theo convention industry (cho ví dụ)?
Đáp án
  1. Content-Type mô tả body thật sự là loại gì; Accept mô tả client đọc được những loại MIME nào. Khi POST JSON body: cần Content-Type: application/json (để server biết parse body là JSON) và Accept: application/json (để server biết trả response bằng JSON). Thiếu Content-Type, server thường không biết parse — trả 415 Unsupported Media Type. Thiếu Accept, server thường giả định */* và mặc định trả JSON cho REST API, nhưng nên set rõ để tránh ambiguity. Ngoài ra request POST cũng nên kèm Authorization: Bearer <JWT> (vì /api/v1/cart/items là endpoint user-scoped).
  2. Bearer token gửi qua header Authorization với format Authorization: Bearer <token> (theo RFC 6750). Khác Basic: (1) Cơ chế — Bearer mang token đã sign sẵn (JWT), server chỉ verify signature mà không cần lookup credential; Basic mang username:password Base64 encoded, server phải hash và so với database mỗi request. (2) Bảo mật — token Bearer có expire ngắn (15 phút) nên thiệt hại nếu lộ giới hạn theo thời gian; Basic gửi nguyên credential gốc mỗi request nên lộ là toàn bộ tài khoản. (3) Cả hai đều bắt buộc HTTPS vì credential/token đi thẳng trong header. Shop API lock Bearer JWT cho mọi endpoint cần auth; Basic chỉ giữ kiến thức để hiểu legacy integration.
  3. no-cache: cho phép lưu vào cache nhưng phải revalidate với server trước khi serve (thường dùng If-None-Match với ETag). Nếu server trả 304, cache serve lại bản local; nếu trả 200, cache cập nhật bản mới. no-store: không được phép lưu gì cả, không cache, không lưu file tạm, không lưu memory. Use case Shop API: GET /api/v1/me trả Cache-Control: private, no-cache — browser cache được nhưng phải revalidate (profile có thể vừa đổi). GET /api/v1/orders/:id/payment trả Cache-Control: no-store — thông tin payment nhạy cảm, cấm mọi cache layer chạm vào.
  4. nginx (hoặc HAProxy, Cloudflare) set X-Forwarded-For: <ip_client_thật>, <ip_proxy_trước_đó> khi forward request xuống axum app. Server đọc header này để lấy IP client. Nhưng không được trust trực tiếp: bất kỳ client nào cũng có thể tự thêm X-Forwarded-For: 1.2.3.4 trong request rồi nếu app trust mù sẽ ghi nhận sai IP — attacker dễ bypass IP-based rate limit và blocking. Quy tắc đúng: chỉ trust khi (a) app đứng sau proxy trong whitelist xác định (IP range của nginx, CIDR của Cloudflare), (b) lấy IP từ vị trí biết trước trong chain (vd phần tử đầu nếu chỉ một lớp proxy, hoặc phần tử thứ N nếu N lớp). Trong axum, đây là vai trò middleware riêng — Shop API sẽ chốt chi tiết ở B158.
  5. Shop API lock bốn custom header: X-Request-Id (UUID v4, link với request_id trong error envelope, propagate qua tracing/logs), Idempotency-Key (client gửi để dedupe POST, theo Stripe convention nên không prefix X-), X-RateLimit-Limit/Remaining/Reset (response header báo quota), X-Total-Count (response header trả tổng record cho list endpoint phân trang). Header nên dùng standard thay vì custom: Authorization (không bao giờ tự custom X-Api-Token), Content-Type (không tự đặt X-Data-Format), ETag (không tự đặt X-Version-Tag), Cache-Control, Retry-After (response 429 và 503 dùng header này, không custom X-Retry-Seconds). Theo convention industry không prefix: Idempotency-Key (Stripe), Forwarded (RFC 7239 thay X-Forwarded-For chuẩn hơn, dù X-Forwarded-* vẫn dùng phổ biến).
11

Bài Tiếp Theo

— đi sâu vào content negotiation với quality value (q=), API versioning qua vendor MIME (application/vnd.shop.v2+json), và Vary header để cache đúng theo client capability.