HTTP: mổ xẻ một request tới order-service
Tiếp đi. HTTPS xong rồi, giờ nhìn kỹ bên trong HTTP.
Bài trước TLS đã bọc đường truyền tới https://api.shop.example. Bên trong lớp TLS vẫn là HTTP. Hiểu HTTP kỹ thì debug API, cấu hình Nginx, tuning HttpClient đều dễ.
1. Bài toán
Mobile app tạo đơn, rồi xem lại đơn. order-service lại gọi inventory-service để giữ hàng. Có lúc bị tạo trùng đơn khi mạng chập chờn, có lúc gọi inventory chậm vô lý.
2. Vấn đề
- Client retry một
POSTsau timeout → tạo 2 đơn. - Trả
200cho mọi thứ, kể cả lỗi → client không biết xử lý. - Mỗi lần gọi inventory mở TCP + TLS mới → tốn thêm vài chục ms.
- Danh sách sản phẩm ít thay đổi nhưng client tải lại toàn bộ mỗi lần.
3. Giải pháp: hiểu đúng các phần của HTTP
Anatomy của request/response (HTTP/1.1 là text):
POST /orders HTTP/1.1 <- request line: method, path, version
Host: api.shop.example <- headers
Content-Type: application/json
Authorization: Bearer <token>
Idempotency-Key: 7f3c-...
<- dòng trống
{"productId": 10, "qty": 2} <- body
HTTP/1.1 201 Created <- status line
Location: /orders/42
Content-Type: application/json
{"id": 42, "status": "NEW"}
Method và idempotency. Safe = không đổi state. Idempotent = gọi N lần, state server như gọi 1 lần.
| Method | Dùng cho | Safe | Idempotent |
|---|---|---|---|
| GET | Đọc | Có | Có |
| POST | Tạo / hành động | Không | Không |
| PUT | Thay toàn bộ resource | Không | Có |
| PATCH | Sửa một phần | Không | Không bắt buộc |
| DELETE | Xóa | Không | Có |
| HEAD / OPTIONS | Header / CORS preflight | Có | Có |
Status code theo nhóm:
| Nhóm | Ý nghĩa | Hay gặp |
|---|---|---|
| 1xx | Thông tin | 101 Switching Protocols (WebSocket) |
| 2xx | Thành công | 200, 201 Created, 204 No Content |
| 3xx | Chuyển hướng / cache | 301, 302, 304 Not Modified |
| 4xx | Lỗi phía client | 400, 401 chưa xác thực, 403 không có quyền, 404, 409 Conflict, 429 Too Many Requests |
| 5xx | Lỗi phía server | 500, 502 Bad Gateway, 503 Unavailable, 504 Gateway Timeout |
Header quan trọng: Host, Content-Type, Accept, Authorization, Content-Length, Connection, Cache-Control, ETag, Set-Cookie, X-Forwarded-For (bài 9.7).
4. Code từng bước
Bước 1: Controller trả status đúng
@RestController
@RequestMapping("/orders")
public class OrderController {
private final OrderService service;
public OrderController(OrderService service) { this.service = service; }
@PostMapping
public ResponseEntity<OrderDto> create(
@RequestHeader("Idempotency-Key") String key,
@RequestBody @Valid CreateOrderRequest req) {
OrderDto order = service.createOnce(key, req); // cùng key → trả lại đơn cũ
return ResponseEntity.created(URI.create("/orders/" + order.id())).body(order);
}
@GetMapping("/{id}")
public ResponseEntity<OrderDto> get(@PathVariable Long id) {
return service.find(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
}
Bước 2: Cache cho danh sách sản phẩm bằng Cache-Control + ETag
@GetMapping("/products")
public ResponseEntity<List<ProductDto>> products() {
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(Duration.ofMinutes(5)).cachePublic())
.body(productService.findAll());
}
// Bật ETag tự động (hash body); client gửi If-None-Match → 304 nếu không đổi
@Bean
FilterRegistrationBean<ShallowEtagHeaderFilter> etagFilter() {
var reg = new FilterRegistrationBean<>(new ShallowEtagHeaderFilter());
reg.addUrlPatterns("/products/*", "/products");
return reg;
}
Bước 3: HttpClient có connection pool khi gọi inventory
// java.net.http.HttpClient: tạo MỘT lần, dùng lại; nó tự pool và keep-alive
@Bean
RestClient inventoryClient(RestClient.Builder builder) {
HttpClient jdk = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(2))
.version(HttpClient.Version.HTTP_2)
.build();
return builder
.baseUrl("http://inventory.internal:8080")
.requestFactory(new JdkClientHttpRequestFactory(jdk))
.build();
}
Bước 4: Test bằng curl
curl -i -X POST https://api.shop.example/orders \
-H "Content-Type: application/json" -H "Idempotency-Key: abc-1" \
-d '{"productId":10,"qty":2}'
# HTTP/2 201 location: /orders/42
curl -i https://api.shop.example/products
# etag: "0a1b2c..." cache-control: max-age=300, public
curl -i https://api.shop.example/products -H 'If-None-Match: "0a1b2c..."'
# HTTP/2 304 (không có body)
# keep-alive: 2 URL trong 1 lệnh, curl dùng lại connection
curl -v https://api.shop.example/orders/42 https://api.shop.example/orders/43 2>&1 | grep -i "re-using"
5. Cách hoạt động: keep-alive và connection reuse
Không reuse (mỗi request 1 connection):
TCP handshake (1 RTT) + TLS (1 RTT) + request/response (1 RTT) = 3 RTT
TCP handshake (1 RTT) + TLS (1 RTT) + request/response (1 RTT) = 3 RTT
... + nhiều socket TIME_WAIT, tốn CPU cho TLS
Keep-alive + pool:
[TCP + TLS 1 lần] ── req1/resp1 ── req2/resp2 ── req3/resp3 ... ~1 RTT mỗi request
HTTP/1.1 mặc định keep-alive. Nhưng nếu code tạo new HttpClient() hay new RestTemplate() mỗi lần gọi thì pool bị bỏ, connection không được dùng lại. Đó là lý do HttpClient phải là singleton bean, và pool cần giới hạn số connection mỗi host.
6. So sánh HTTP/1.1 và HTTP/2
| HTTP/1.1 | HTTP/2 | |
|---|---|---|
| Định dạng | Text | Binary frame |
| Đồng thời trên 1 connection | 1 request tại một thời điểm (head-of-line blocking) | Multiplexing nhiều stream |
| Header | Gửi lặp lại nguyên văn | Nén bằng HPACK |
| Số connection | Browser mở ~6/host | Thường 1/host |
| TLS | Tùy chọn | Browser chỉ hỗ trợ qua TLS (h2) |
HTTP/3 chạy trên QUIC (UDP), bỏ head-of-line blocking ở tầng TCP. Với backend Java, thường bật HTTP/2 ở Nginx/LB phía client, còn Nginx → app vẫn HTTP/1.1 keep-alive.
7. Khi nào nên dùng / không nên dùng
- Cache-Control dài cho dữ liệu công khai ít đổi;
no-storecho dữ liệu cá nhân, đơn hàng. - ETag khi muốn tiết kiệm băng thông.
ShallowEtagHeaderFiltervẫn chạy controller, chỉ tiết kiệm mạng, không tiết kiệm CPU/DB. - Idempotency-Key cho POST tạo tiền/đơn hàng khi client có retry.
Có GET, POST, PUT, DELETE là đủ rồi, sao còn cần thêm PATCH?
Bạn hiểu gần đúng, chỉ chỉnh một điểm: không có method UPDATE, đó là PUT. Và đúng là GET, POST, PUT, DELETE đủ để làm gần như mọi thứ. PATCH tồn tại để diễn đạt rõ "chỉ sửa một phần".
Đơn 42 hiện tại: {"id":42, "status":"NEW", "note":"giao giờ hành chính", "qty":2}
PUT /orders/42 → "đây là trạng thái MỚI của toàn bộ đơn 42"
{"status":"NEW", "note":"giao buổi tối", "qty":2}
PATCH /orders/42 → "giữ nguyên, chỉ sửa note"
{"note":"giao buổi tối"}
Không có PATCH vẫn code được (PUT mà backend "field nào gửi thì sửa field đó"), nhưng semantics của API không rõ bằng. Điểm hay bị hỏi tiếp: PUT nên idempotent, PATCH không nhất thiết. {"note":"x"} gọi 2 lần vẫn vậy, nhưng {"incrementQty": 1} gọi 2 lần là tăng 2.
Chi tiết thiết kế API (JSON Merge Patch, validation, 404 vs 409) xem ở chương REST API (bài PUT vs PATCH).
Cookie thì liên quan gì HTTP? Nó lưu ở đâu?
Đúng ý rồi: HTTP là stateless, mỗi request độc lập. Cookie là cơ chế header để "nhớ" giữa các request.
1. POST /admin/login → HTTP/1.1 200
Set-Cookie: SESSION=8f2a...; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1800
2. Browser lưu cookie theo domain api.shop.example
3. GET /admin/orders → Cookie: SESSION=8f2a...
4. Server dùng SESSION id tra session (memory/Redis)
| Thuộc tính | Tác dụng |
|---|---|
| Secure | Chỉ gửi qua HTTPS |
| HttpOnly | JavaScript không đọc được, giảm hại khi bị XSS |
| SameSite | Hạn chế gửi cookie cross-site, chống CSRF |
| Domain / Path | Phạm vi gửi cookie |
| Max-Age / Expires | Không có thì là session cookie, đóng browser là mất |
Session lưu trong memory của một instance sẽ gây chuyện khi ta chạy 2 instance sau load balancer. Bài 9.7 sẽ xử lý.
Phỏng vấn hỏi về HTTP thì trả lời sao?
Q1. Idempotent là gì? Method nào idempotent?
🇻🇳 Tiếng Việt:
Idempotent là gọi một lần hay nhiều lần thì trạng thái server như nhau. GET, PUT, DELETE, HEAD, OPTIONS idempotent; POST không, PATCH không bắt buộc. Với POST tạo đơn hàng, em dùng Idempotency-Key để client retry an toàn.
🇬🇧 English:
An idempotent request leaves the server in the same state whether it is called once or many times. GET, PUT, DELETE, HEAD and OPTIONS are idempotent; POST is not, and PATCH is not guaranteed to be. For order creation via POST, I use an Idempotency-Key header so clients can retry safely.
Q2. PUT khác PATCH thế nào?
🇻🇳 Tiếng Việt:
Cả hai đều cập nhật resource. PUT thay toàn bộ resource bằng representation gửi lên và nên idempotent. PATCH chỉ sửa một phần, client gửi các field cần đổi, và không nhất thiết idempotent.
🇬🇧 English:
Both update a resource. PUT replaces the whole resource with the representation sent and should be idempotent. PATCH applies a partial update with only the changed fields, and it is not necessarily idempotent.
Q3. Vì sao phải dùng lại HttpClient thay vì tạo mới mỗi request?
🇻🇳 Tiếng Việt:
HttpClient giữ connection pool. Dùng lại thì connection keep-alive được tái sử dụng, bỏ qua TCP và TLS handshake, giảm latency và số socket TIME_WAIT. Tạo mới mỗi lần sẽ mất lợi ích này và có thể cạn tài nguyên khi tải cao.
🇬🇧 English:
An HttpClient owns a connection pool. Reusing it lets keep-alive connections be reused, skipping TCP and TLS handshakes, which cuts latency and TIME_WAIT sockets. Creating a new client per request throws that away and can exhaust resources under load.
Q4. HTTP/2 khác HTTP/1.1 ở điểm nào chính?
🇻🇳 Tiếng Việt:
HTTP/2 dùng binary frame, multiplex nhiều stream trên một connection và nén header bằng HPACK, nên tránh head-of-line blocking ở tầng HTTP mà HTTP/1.1 gặp.
🇬🇧 English:
HTTP/2 uses binary framing, multiplexes many streams over one connection and compresses headers with HPACK, which removes the HTTP-level head-of-line blocking of HTTP/1.1.
Cách nhớ ngắn gọn
- Request = method + path + headers + body; response = status + headers + body.
- 4xx lỗi client, 5xx lỗi server; 502/504 là lỗi ở proxy.
- POST không idempotent → Idempotency-Key.
- HttpClient singleton = connection pool = keep-alive.
- Cache-Control quyết định có cache; ETag giúp 304.
Tự kiểm tra
401 khác 403 thế nào?
401: chưa xác thực hoặc token sai. 403: đã biết bạn là ai nhưng không có quyền.
DELETE gọi 2 lần, lần 2 trả 404. Có còn idempotent không?
Có. Idempotency nói về trạng thái server, không phải status code. Sau 1 hay 2 lần, đơn đều đã bị xóa.
ETag và If-None-Match phối hợp ra sao?
Server trả ETag. Lần sau client gửi If-None-Match với giá trị đó; nếu không đổi, server trả 304 không body.
Tạo HttpClient mới trong mỗi lần gọi gây hậu quả gì?
Mất connection reuse: mỗi lần lại TCP + TLS handshake, latency tăng, nhiều socket TIME_WAIT.
Cookie HttpOnly và SameSite dùng để làm gì?
HttpOnly chặn JavaScript đọc cookie (giảm hại XSS). SameSite hạn chế gửi cookie cross-site (chống CSRF).