HTTP: mổ xẻ một request tới order-service

4 câu hỏi~25 phútTừ khóa: request/response, method, idempotent, status code, header, keep-alive, connection pool, HTTP/2, Cache-Control, ETag, cookie

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 đề

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.

MethodDùng choSafeIdempotent
GETĐọcCóCó
POSTTạo / hành độngKhôngKhông
PUTThay toàn bộ resourceKhôngCó
PATCHSửa một phầnKhôngKhông bắt buộc
DELETEXóaKhôngCó
HEAD / OPTIONSHeader / CORS preflightCóCó

Status code theo nhóm:

NhómÝ nghĩaHay gặp
1xxThông tin101 Switching Protocols (WebSocket)
2xxThành công200, 201 Created, 204 No Content
3xxChuyển hướng / cache301, 302, 304 Not Modified
4xxLỗi phía client400, 401 chưa xác thực, 403 không có quyền, 404, 409 Conflict, 429 Too Many Requests
5xxLỗi phía server500, 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.1HTTP/2
Định dạngTextBinary frame
Đồng thời trên 1 connection1 request tại một thời điểm (head-of-line blocking)Multiplexing nhiều stream
HeaderGửi lặp lại nguyên vănNén bằng HPACK
Số connectionBrowser mở ~6/hostThường 1/host
TLSTùy chọnBrowser 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

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ínhTác dụng
SecureChỉ gửi qua HTTPS
HttpOnlyJavaScript không đọc được, giảm hại khi bị XSS
SameSiteHạn chế gửi cookie cross-site, chống CSRF
Domain / PathPhạm vi gửi cookie
Max-Age / ExpiresKhô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


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).