Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog
in java

http abstractions

Tung Bui Thanh (TungDaDev)16 mins read

Http abstractions.webp

# giới thiệu

HTTP là thứ ngôn ngữ mà REST API nào cũng nói. Có điều viết HTTP thô trong Java khá dài dòng: chỉ một GET request với HttpURLConnection đã tốn cả chục dòng code. Spring gói các khái niệm của HTTP (status, header, media type, request, response) thành những object Java có kiểu rõ ràng để mình làm việc cho đỡ cực.

Package org.springframework.http là bộ từ vựng bạn dùng hằng ngày: ResponseEntity gói response kèm status và headers, HttpHeaders quản lý header có kiểu, MediaType thay cho chuỗi "application/json" gõ tay. Thêm vào đó là mấy HTTP client (RestTemplate, WebClient, RestClient) để gọi sang service khác.

Nắm được mấy class này thì bạn hiểu controller trả response ra sao và các service trong hệ microservices gọi nhau thế nào.

# http-status: http status codes

Status code là "ngôn ngữ cơ thể" của API. Client không cần parse body mới biết request thành hay bại, nhìn status code là đủ: 200 là OK, 404 là không tìm thấy, 500 là server lỗi.

Spring có enum HttpStatus chứa các mã chuẩn kèm vài hàm tiện (is2xxSuccessful(), is4xxClientError()). Dùng enum thay cho magic number thì code dễ đọc hơn, IDE lại gợi ý sẵn nên khó nhớ nhầm. Từ Spring 6, kiểu chung là interface HttpStatusCode (status nhận từ server khác có thể là mã không nằm trong enum), còn HttpStatus là enum cài đặt interface đó.

Một chi tiết nhỏ: RFC 9110 đổi tên 422 thành "Unprocessable Content", nên Spring Framework 7 thêm HttpStatus.UNPROCESSABLE_CONTENT và đánh dấu UNPROCESSABLE_ENTITY là deprecated. Code chạy Spring 6 thì vẫn dùng tên cũ.

// Các status thường dùng
HttpStatus.OK                    // 200
HttpStatus.CREATED               // 201
HttpStatus.ACCEPTED              // 202
HttpStatus.NO_CONTENT            // 204
HttpStatus.BAD_REQUEST           // 400
HttpStatus.UNAUTHORIZED          // 401
HttpStatus.FORBIDDEN             // 403
HttpStatus.NOT_FOUND             // 404
HttpStatus.METHOD_NOT_ALLOWED    // 405
HttpStatus.CONFLICT              // 409
HttpStatus.UNPROCESSABLE_ENTITY  // 422 (Spring 7: UNPROCESSABLE_CONTENT)
HttpStatus.TOO_MANY_REQUESTS     // 429
HttpStatus.INTERNAL_SERVER_ERROR // 500
HttpStatus.SERVICE_UNAVAILABLE   // 503
 
// Check category
HttpStatus status = HttpStatus.OK;
status.is2xxSuccessful();  // true
status.is4xxClientError(); // false
status.is5xxServerError(); // false
status.isError();          // false
 
// Value
status.value();            // 200
status.getReasonPhrase();  // "OK"
status.series();           // HttpStatus.Series.SUCCESSFUL

# responseentity: full http response control

Đây là class bạn dùng nhiều nhất khi viết REST controller. ResponseEntity cho bạn toàn quyền với response: status code, headers và body.

Khi nào dùng ResponseEntity, khi nào return object luôn? Cứ thế này: method lúc nào cũng trả 200 OK thì return object cho gọn. Cần status khác (201 Created, 204 No Content), cần header riêng (Location, ETag) hay response có điều kiện (304 Not Modified) thì dùng ResponseEntity.

Mấy pattern quen thuộc: POST trả 201 kèm header Location trỏ tới resource vừa tạo. DELETE trả 204, body rỗng. GET có thể trả 304 nếu bản client đang cache vẫn là bản mới nhất (so khớp ETag).

Lưu ý trong ví dụ dưới, method dùng ETag được map vào path riêng. Nếu hai method cùng @GetMapping("/{id}") trong một controller, Spring sẽ báo "Ambiguous mapping" ngay lúc khởi động.

@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
 
   // 200 OK với body
   @GetMapping("/{id}")
   public ResponseEntity<APIResponse<ProductDTO>> get(@PathVariable UUID id) {
       ProductDTO product = productService.getById(id);
       return ResponseEntity.ok(APIResponse.success(product));
   }
 
   // 201 Created với Location header
   @PostMapping
   public ResponseEntity<APIResponse<ProductDTO>> create(@Valid @RequestBody CreateRequest req) {
       ProductDTO created = productService.create(req);
       URI location = ServletUriComponentsBuilder.fromCurrentRequest()
           .path("/{id}")
           .buildAndExpand(created.getId())
           .toUri();
       return ResponseEntity.created(location).body(APIResponse.success(created));
   }
 
   // 204 No Content
   @DeleteMapping("/{id}")
   public ResponseEntity<Void> delete(@PathVariable UUID id) {
       productService.delete(id);
       return ResponseEntity.noContent().build();
   }
 
   // 202 Accepted (async processing)
   @PostMapping("/import")
   public ResponseEntity<Void> importAsync(@RequestBody ImportRequest request) {
       String jobId = jobService.submit(request);
       return ResponseEntity.accepted()
           .header("X-Job-Id", jobId)
           .header("Location", "/api/v1/jobs/" + jobId)
           .build();
   }
 
   // Custom status + headers
   @GetMapping("/export")
   public ResponseEntity<byte[]> export() {
       byte[] data = exportService.generateCsv();
       return ResponseEntity.ok()
           .contentType(MediaType.parseMediaType("text/csv"))
           .contentLength(data.length)
           .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"export.csv\"")
           .header(HttpHeaders.CACHE_CONTROL, "no-cache")
           .body(data);
   }
 
   // Conditional — ETag / 304 Not Modified
   // (path khác với get() ở trên, trùng path thì Spring báo Ambiguous mapping)
   @GetMapping("/{id}/cached")
   public ResponseEntity<ProductDTO> getWithEtag(@PathVariable UUID id, WebRequest request) {
       ProductDTO product = productService.getById(id);
       String etag = "\"v" + product.getVersion() + "\"";
 
       if (request.checkNotModified(etag)) {
           return null; // Spring returns 304 automatically
       }
 
       return ResponseEntity.ok()
           .eTag(etag)
           .lastModified(product.getUpdatedAt().toInstant(ZoneOffset.UTC))
           .cacheControl(CacheControl.maxAge(Duration.ofMinutes(5)))
           .body(product);
   }
 
   // Error responses
   @ExceptionHandler(EntityNotFoundException.class)
   public ResponseEntity<APIResponse<Void>> handleNotFound(EntityNotFoundException ex) {
       return ResponseEntity.status(HttpStatus.NOT_FOUND)
           .body(APIResponse.error("NOT_FOUND", ex.getMessage()));
   }
}

# responseentity builder methods

MethodStatusUse Case
ResponseEntity.ok(body)200Successful GET/PUT/PATCH
ResponseEntity.created(uri).body(b)201Successful POST
ResponseEntity.accepted()202Async processing accepted
ResponseEntity.noContent().build()204Successful DELETE
ResponseEntity.badRequest().body(b)400Validation error
ResponseEntity.notFound().build()404Resource not found
ResponseEntity.status(code).body(b)CustomAny status
ResponseEntity.unprocessableEntity()422Business rule violation (Spring 7: unprocessableContent())

# HttpHeaders: http header management

Header mang metadata của request/response: token xác thực, content type, chỉ thị cache, cả context nghiệp vụ riêng (workspace ID, trace ID). Class HttpHeaders có sẵn các getter/setter đúng kiểu cho header chuẩn, đỡ phải tự xử lý chuỗi.

Trong hệ microservices CSP, header chở context đi suốt chuỗi request: X-Workspace-Id xác định tenant, X-Request-Id để trace, Authorization mang JWT. Service nào cũng đọc mấy header này để biết "ai đang gọi, trong context nào".

// Tạo headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setAccept(List.of(MediaType.APPLICATION_JSON));
headers.setBearerAuth("eyJhbGciOiJSUzI1NiIs...");
headers.set("X-Workspace-Id", workspaceId.toString());
headers.set("X-Request-Id", UUID.randomUUID().toString());
headers.setCacheControl(CacheControl.noCache());
 
// Common header constants
HttpHeaders.AUTHORIZATION       // "Authorization"
HttpHeaders.CONTENT_TYPE        // "Content-Type"
HttpHeaders.ACCEPT              // "Accept"
HttpHeaders.CACHE_CONTROL       // "Cache-Control"
HttpHeaders.CONTENT_DISPOSITION // "Content-Disposition"
HttpHeaders.CONTENT_LENGTH      // "Content-Length"
HttpHeaders.LOCATION            // "Location"
HttpHeaders.ETAG                // "ETag"
HttpHeaders.IF_NONE_MATCH       // "If-None-Match"
HttpHeaders.IF_MODIFIED_SINCE   // "If-Modified-Since"
HttpHeaders.COOKIE              // "Cookie"
HttpHeaders.SET_COOKIE          // "Set-Cookie"
HttpHeaders.ORIGIN              // "Origin"
HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN // "Access-Control-Allow-Origin"
 
// Read headers in controller
@GetMapping("/debug-headers")
public void inspectHeaders(@RequestHeader HttpHeaders headers) {
   String auth = headers.getFirst(HttpHeaders.AUTHORIZATION);
   MediaType contentType = headers.getContentType();
   long contentLength = headers.getContentLength();
   List<MediaType> accepts = headers.getAccept();
}
 
// Content-Disposition for file download
ContentDisposition disposition = ContentDisposition.attachment()
   .filename("report.pdf", StandardCharsets.UTF_8)
   .build();
headers.setContentDisposition(disposition);

# mediatype: content types

Header Content-Type cho biết body đang ở định dạng gì. Gõ tay chuỗi "application/json" khắp nơi thì sớm muộn cũng có chỗ gõ sai. Hằng số MediaType có kiểu rõ ràng, IDE gợi ý được, lại có sẵn hàm để parse và kiểm tra tương thích.

Bạn dùng nó trong produces/consumes của controller, trong HttpHeaders.setContentType() và khi dựng HTTP client. Spring dựa vào MediaType để chọn đúng HttpMessageConverter (Jackson cho JSON, Jackson XML hoặc JAXB cho XML...).

// Predefined constants
MediaType.APPLICATION_JSON               // application/json
MediaType.APPLICATION_JSON_VALUE         // "application/json" (String)
MediaType.APPLICATION_XML                // application/xml
MediaType.APPLICATION_OCTET_STREAM      // application/octet-stream (binary)
MediaType.APPLICATION_PDF               // application/pdf
MediaType.APPLICATION_FORM_URLENCODED   // application/x-www-form-urlencoded
MediaType.MULTIPART_FORM_DATA           // multipart/form-data
MediaType.TEXT_PLAIN                     // text/plain
MediaType.TEXT_HTML                      // text/html
MediaType.TEXT_EVENT_STREAM             // text/event-stream (SSE)
MediaType.IMAGE_PNG                     // image/png
MediaType.IMAGE_JPEG                    // image/jpeg
 
// Custom media type
MediaType custom = new MediaType("application", "vnd.tungdadev.v2+json");
MediaType csv = MediaType.parseMediaType("text/csv;charset=UTF-8");
 
// Check compatibility
MediaType.APPLICATION_JSON.isCompatibleWith(MediaType.parseMediaType("application/json"));
 
// Usage in controller
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
@PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE)
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)

# RequestEntity & HttpEntity: http request representation

HttpEntity gói headers và body vào một object, tiện khi truyền vào RestTemplate. RequestEntity thêm HTTP method và URI, tức là mô tả trọn một HTTP request.

Bạn sẽ gặp chúng chủ yếu khi viết HTTP client để gọi service khác. Còn ở controller thì Spring đã parse request giùm qua @RequestBody, @RequestHeader, ít khi phải đụng trực tiếp.

// HttpEntity — base class (headers + body)
HttpEntity<CreateProductRequest> entity = new HttpEntity<>(request, headers);
 
// RequestEntity — includes method + URL
RequestEntity<CreateProductRequest> requestEntity = RequestEntity
   .post(URI.create("http://product-service/api/v1/products"))
   .contentType(MediaType.APPLICATION_JSON)
   .header("X-Workspace-Id", workspaceId.toString())
   .header("Authorization", "Bearer " + token)
   .body(request);
 
// GET without body
RequestEntity<Void> getRequest = RequestEntity
   .get(URI.create("http://product-service/api/v1/products/" + id))
   .accept(MediaType.APPLICATION_JSON)
   .header("Authorization", "Bearer " + token)
   .build();

# RestTemplate (classic http client)

RestTemplate là HTTP client blocking đời đầu của Spring, có từ Spring 3.0. Mỗi request giữ thread đứng chờ cho tới khi có response.

Trong CSP, các service gọi nhau đồng bộ qua REST. RestTemplate đơn giản, dễ hiểu, dễ debug, đủ dùng cho phần lớn trường hợp không cần lập trình reactive.

Lưu ý: RestTemplate đã ở "maintenance mode" từ lâu, chỉ sửa lỗi chứ không thêm tính năng. Tháng 9/2025, Spring team công bố lộ trình: Spring Framework 7.1 (dự kiến 11/2026) sẽ đánh dấu @Deprecated và 8.0 sẽ gỡ hẳn. Project mới nên dùng RestClient (Spring 6.1+): cùng mô hình blocking, API hiện đại hơn. Code cũ thì chưa phải vội, nhưng nên lên kế hoạch chuyển dần.

Thêm hai chi tiết hay vấp. setConnectTimeout/setReadTimeout của RestTemplateBuilder bị deprecated từ Spring Boot 3.4 và không còn ở Boot 4, nên ví dụ dưới dùng connectTimeout/readTimeout. Còn patchForObject cần request factory hỗ trợ PATCH: SimpleClientHttpRequestFactory (dựa trên HttpURLConnection) không gửi được PATCH, phải dùng Apache HttpClient, Jetty hoặc JDK HttpClient. RestTemplateBuilder của Boot thường tự chọn factory phù hợp theo thư viện có trên classpath.

@Configuration
public class RestTemplateConfig {
 
   @Bean
   public RestTemplate restTemplate(RestTemplateBuilder builder) {
       return builder
           .connectTimeout(Duration.ofSeconds(5))   // Boot < 3.4: setConnectTimeout
           .readTimeout(Duration.ofSeconds(10))     // Boot < 3.4: setReadTimeout
           .rootUri("http://localhost:8080")
           .defaultHeader("X-Source", "csp-console")
           .errorHandler(new CustomErrorHandler())
           .interceptors(new LoggingInterceptor())
           .build();
   }
}
 
@Service
@RequiredArgsConstructor
@Slf4j
public class ProductClient {
 
   private final RestTemplate restTemplate;
 
   // GET
   public ProductDTO getProduct(UUID id) {
       return restTemplate.getForObject("/api/v1/products/{id}", ProductDTO.class, id);
   }
 
   // GET with ResponseEntity (access headers, status)
   public ResponseEntity<ProductDTO> getProductEntity(UUID id) {
       return restTemplate.getForEntity("/api/v1/products/{id}", ProductDTO.class, id);
   }
 
   // POST
   public ProductDTO createProduct(CreateProductRequest request) {
       return restTemplate.postForObject("/api/v1/products", request, ProductDTO.class);
   }
 
   // PUT
   public void updateProduct(UUID id, UpdateProductRequest request) {
       restTemplate.put("/api/v1/products/{id}", request, id);
   }
 
   // DELETE
   public void deleteProduct(UUID id) {
       restTemplate.delete("/api/v1/products/{id}", id);
   }
 
   // Exchange (full control)
   public Page<ProductDTO> searchProducts(String keyword, int page) {
       HttpHeaders headers = new HttpHeaders();
       headers.setBearerAuth(getToken());
 
       HttpEntity<Void> entity = new HttpEntity<>(headers);
 
       ResponseEntity<RestPageResponse<ProductDTO>> response = restTemplate.exchange(
           "/api/v1/products?keyword={keyword}&page={page}",
           HttpMethod.GET,
           entity,
           new ParameterizedTypeReference<>() {},
           keyword, page
       );
 
       return response.getBody();
   }
 
   // PATCH
   public ProductDTO patchProduct(UUID id, Map<String, Object> updates) {
       HttpHeaders headers = new HttpHeaders();
       headers.setContentType(MediaType.APPLICATION_JSON);
       HttpEntity<Map<String, Object>> entity = new HttpEntity<>(updates, headers);
 
       return restTemplate.patchForObject("/api/v1/products/{id}", entity, ProductDTO.class, id);
   }
}

# WebClient (reactive http client, spring webflux)

WebClient là HTTP client non-blocking theo kiểu reactive, sinh ra cho các tình huống tải cao. Trong lúc chờ response nó không giữ thread, nhờ vậy một thread phục vụ được nhiều request đồng thời.

Khi nào nên dùng WebClient thay RestTemplate?

  • Cần gọi nhiều external services song song (fan-out pattern)
  • Application đã dùng WebFlux/reactive stack
  • Cần streaming responses (SSE, large file downloads)
  • Cần built-in retry, timeout, backpressure

Dù là reactive, WebClient vẫn dùng kiểu blocking được (.block()) trong app servlet. Trước khi có RestClient, nhiều team dùng WebClient cho cả hai kiểu vì API fluent và nhiều tính năng hơn RestTemplate. Giờ thì nếu app thuần servlet và chỉ cần gọi blocking, RestClient gọn hơn: khỏi kéo cả WebFlux và Reactor vào chỉ để gọi .block().

Ví dụ dưới có hai chỗ đáng để ý. onStatus chỉ đổi 404 thành ResourceNotFoundException, vì gom mọi 4xx (400, 401, 403...) thành "not found" sẽ che mất lỗi thật. Và retryWhen chỉ retry lỗi 5xx: retry một request bị 4xx thì lần nào cũng nhận lại đúng lỗi đó.

@Configuration
public class WebClientConfig {
 
   @Bean
   public WebClient webClient() {
       return WebClient.builder()
           .baseUrl("http://product-service")
           .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
           .filter(ExchangeFilterFunctions.basicAuthentication("user", "pass"))
           .codecs(config -> config.defaultCodecs().maxInMemorySize(16 * 1024 * 1024))
           .build();
   }
}
 
@Service
@RequiredArgsConstructor
public class ProductWebClient {
 
   private final WebClient webClient;
 
   // GET — blocking
   public ProductDTO getProduct(UUID id) {
       return webClient.get()
           .uri("/api/v1/products/{id}", id)
           .header("Authorization", "Bearer " + token)
           .retrieve()
           .onStatus(status -> status.value() == 404, response ->
               Mono.error(new ResourceNotFoundException("Product not found")))
           .bodyToMono(ProductDTO.class)
           .block();  // Block for synchronous usage
   }
 
   // GET — reactive
   public Mono<ProductDTO> getProductReactive(UUID id) {
       return webClient.get()
           .uri("/api/v1/products/{id}", id)
           .retrieve()
           .bodyToMono(ProductDTO.class);
   }
 
   // GET list
   public List<ProductDTO> getProducts(String keyword) {
       return webClient.get()
           .uri(uriBuilder -> uriBuilder
               .path("/api/v1/products")
               .queryParam("keyword", keyword)
               .queryParam("size", 50)
               .build())
           .retrieve()
           .bodyToFlux(ProductDTO.class)
           .collectList()
           .block();
   }
 
   // POST
   public ProductDTO createProduct(CreateProductRequest request) {
       return webClient.post()
           .uri("/api/v1/products")
           .bodyValue(request)
           .retrieve()
           .bodyToMono(ProductDTO.class)
           .block();
   }
 
   // PUT with error handling
   public ProductDTO updateProduct(UUID id, UpdateProductRequest request) {
       return webClient.put()
           .uri("/api/v1/products/{id}", id)
           .bodyValue(request)
           .retrieve()
           .onStatus(HttpStatusCode::is4xxClientError, response ->
               response.bodyToMono(String.class)
                   .flatMap(body -> Mono.error(new ClientException(body))))
           .onStatus(HttpStatusCode::is5xxServerError, response ->
               Mono.error(new ServiceUnavailableException("Product service down")))
           .bodyToMono(ProductDTO.class)
           .timeout(Duration.ofSeconds(10))
           .retryWhen(Retry.backoff(3, Duration.ofSeconds(1))
               .filter(ex -> ex instanceof ServiceUnavailableException)) // chỉ retry lỗi 5xx
           .block();
   }
 
   // DELETE
   public void deleteProduct(UUID id) {
       webClient.delete()
           .uri("/api/v1/products/{id}", id)
           .retrieve()
           .toBodilessEntity()
           .block();
   }
}

# RestClient (spring 6.1+, modern blocking client)

RestClient giống như "RestTemplate 2.0": vẫn blocking, nhưng API fluent theo phong cách WebClient. Nếu bạn đang ở Spring Boot 3.2+ thì đây là lựa chọn nên dùng cho các call HTTP blocking.

API giống WebClient (.get().uri().retrieve().body()) nhưng chạy đồng bộ: không cần .block(), không cần biết Mono/Flux là gì. Đơn giản như RestTemplate mà viết thì fluent như WebClient.

Trong app Spring Boot, nên inject bean RestClient.Builder mà Boot đã cấu hình sẵn (message converter, observability...) thay vì tự gọi RestClient.builder() như ví dụ dưới; tự tạo builder thì bạn mất phần cấu hình đó.

@Configuration
public class RestClientConfig {
 
   @Bean
   public RestClient restClient(TokenProvider tokenProvider) {
       return RestClient.builder()
           .baseUrl("http://product-service")
           .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
           .requestInterceptor((request, body, execution) -> {
               request.getHeaders().setBearerAuth(tokenProvider.getToken());
               return execution.execute(request, body);
           })
           .build();
   }
}
 
@Service
@RequiredArgsConstructor
public class ProductRestClient {
 
   private final RestClient restClient;
 
   public ProductDTO getProduct(UUID id) {
       return restClient.get()
           .uri("/api/v1/products/{id}", id)
           .retrieve()
           .body(ProductDTO.class);
   }
 
   public ProductDTO createProduct(CreateProductRequest request) {
       return restClient.post()
           .uri("/api/v1/products")
           .body(request)
           .retrieve()
           .body(ProductDTO.class);
   }
 
   public ResponseEntity<ProductDTO> getWithStatus(UUID id) {
       return restClient.get()
           .uri("/api/v1/products/{id}", id)
           .retrieve()
           .toEntity(ProductDTO.class);
   }
 
   public void deleteProduct(UUID id) {
       restClient.delete()
           .uri("/api/v1/products/{id}", id)
           .retrieve()
           .toBodilessEntity();
   }
}

# CacheControl: http caching

HTTP caching là cách tăng hiệu năng gần như không tốn gì: client hoặc CDN giữ lại response, server đỡ tải. Nhưng cache sai thì user thấy dữ liệu cũ. Builder CacheControl giúp bạn đặt đúng chỉ thị.

Cách chọn thường gặp: tài nguyên tĩnh (ảnh, CSS) để max-age dài. Response động của API thì max-age ngắn kèm must-revalidate, hoặc kiểm tra lại bằng ETag. Dữ liệu nhạy cảm (hồ sơ user, thanh toán) thì no-store. Phân biệt thêm public (CDN, proxy được cache) và private (chỉ trình duyệt của user được cache): response có dữ liệu riêng của từng user mà để public thì CDN có thể trả nhầm cho người khác.

@GetMapping("/{id}")
public ResponseEntity<ProductDTO> get(@PathVariable UUID id) {
   ProductDTO product = productService.getById(id);
   return ResponseEntity.ok()
       .cacheControl(CacheControl.maxAge(Duration.ofMinutes(5))
           .mustRevalidate()
           .cachePublic())
       .body(product);
}
 
// No cache
@GetMapping("/me")
public ResponseEntity<UserDTO> getCurrentUser() {
   return ResponseEntity.ok()
       .cacheControl(CacheControl.noStore())
       .body(userService.getCurrentUser());
}
 
// CacheControl options
CacheControl.maxAge(Duration.ofHours(1))       // max-age=3600
CacheControl.maxAge(60, TimeUnit.SECONDS)      // max-age=60
CacheControl.noCache()                          // no-cache (revalidate every time)
CacheControl.noStore()                          // no-store (never cache)
CacheControl.empty()                            // No directives
   .mustRevalidate()                           // must-revalidate
   .cachePublic()                              // public  (chọn một trong public/private)
   .cachePrivate()                             // private
   .sMaxAge(Duration.ofMinutes(10))           // s-maxage (shared cache)
   .staleWhileRevalidate(Duration.ofSeconds(30)) // stale-while-revalidate

# HttpMethod: các http method

Đơn giản mà cần: các hằng số cho HTTP method, dùng thay cho chuỗi gõ tay khi dựng request động hay cấu hình method được phép cho CORS. Một chi tiết: từ Spring 6, HttpMethod không còn là enum mà là class (để biểu diễn được cả method lạ ngoài danh sách chuẩn). Vì vậy không switch trên nó như enum được nữa, nhưng các hằng số HttpMethod.GET... và valueOf() vẫn dùng như cũ.

HttpMethod.GET
HttpMethod.POST
HttpMethod.PUT
HttpMethod.PATCH
HttpMethod.DELETE
HttpMethod.HEAD
HttpMethod.OPTIONS
HttpMethod.TRACE
 
// Usage with RestTemplate
restTemplate.exchange(url, HttpMethod.PATCH, entity, ProductDTO.class);
 
// Check method
HttpMethod method = HttpMethod.valueOf("POST");
method.matches("POST");  // true

Cookie giữ state giữa client và server (session ID, refresh token, tuỳ chọn của user). Builder ResponseCookie giúp bạn đặt đủ các thuộc tính bảo mật: HttpOnly (JavaScript không đọc được), Secure (chỉ gửi qua HTTPS), SameSite (giảm rủi ro CSRF).

Trong CSP dùng OAuth2/JWT, access token thường nằm ở header, còn refresh token có thể để trong cookie HttpOnly, an toàn hơn vì JavaScript (kể cả script do lỗi XSS chèn vào) không đọc được.

Khi xoá cookie, path và domain phải khớp với lúc tạo. Cookie tạo với .domain(".tungdadev.com") mà lệnh xoá không có domain thì trình duyệt coi là một cookie khác, cookie cũ vẫn nằm đó.

// Tạo cookie
ResponseCookie cookie = ResponseCookie.from("session_id", sessionId)
   .httpOnly(true)
   .secure(true)
   .path("/")
   .maxAge(Duration.ofHours(24))
   .sameSite("Strict")
   .domain(".tungdadev.com")
   .build();
 
// Set trong response
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
   LoginResponse response = authService.login(request);
   ResponseCookie cookie = ResponseCookie.from("refresh_token", response.getRefreshToken())
       .httpOnly(true).secure(true).path("/api/auth").maxAge(Duration.ofDays(7)).build();
 
   return ResponseEntity.ok()
       .header(HttpHeaders.SET_COOKIE, cookie.toString())
       .body(response);
}
 
// Delete cookie
ResponseCookie deleteCookie = ResponseCookie.from("session_id", "")
   .maxAge(0)
   .path("/")
   .domain(".tungdadev.com") // path + domain phải khớp lúc tạo
   .build();

# HttpStatusCode & ProblemDetail (spring 6+)

ProblemDetail cài đặt chuẩn "Problem Details for HTTP APIs", ban đầu là RFC 7807, đến 7/2023 được thay bằng RFC 9457 (giữ nguyên các field, chủ yếu làm rõ thêm). Thay vì mỗi team tự đặt một kiểu JSON lỗi, chuẩn này cho một cấu trúc chung: type (URI định danh loại lỗi), title, status, detail, instance.

Lợi ích là bên gọi API biết chắc response lỗi trông thế nào, nên parse, hiển thị hay log đều dễ. Spring 6 hỗ trợ sẵn: return ProblemDetail từ @ExceptionHandler là xong; nếu bạn không set instance, Spring tự điền path của request. Muốn các exception có sẵn của Spring MVC (validation, 405, 415...) cũng trả về dạng này thì bật spring.mvc.problemdetails.enabled=true trong Spring Boot.

// ProblemDetail — RFC 7807 error response
@ExceptionHandler(ProductNotFoundException.class)
public ProblemDetail handleNotFound(ProductNotFoundException ex) {
   ProblemDetail problem = ProblemDetail.forStatusAndDetail(
       HttpStatus.NOT_FOUND, ex.getMessage());
   problem.setTitle("Product Not Found");
   problem.setType(URI.create("https://api.tungdadev.com/errors/product-not-found"));
   problem.setProperty("productId", ex.getProductId());
   problem.setProperty("timestamp", Instant.now());
   return problem;
}

Output (RFC 9457/7807):

{
  "type": "https://api.tungdadev.com/errors/product-not-found",
  "title": "Product Not Found",
  "status": 404,
  "detail": "Product with ID abc-123 not found",
  "instance": "/api/v1/products/abc-123",
  "productId": "abc-123",
  "timestamp": "2024-01-15T10:30:00Z"
}

# quick reference

Class/InterfaceMục đích
HttpStatusHTTP status code enum
HttpStatusCodeStatus code interface (Spring 6)
ResponseEntityFull response (status + headers + body)
RequestEntityFull request (method + URI + headers + body)
HttpEntityHeaders + body wrapper
HttpHeadersHTTP header manipulation
MediaTypeContent-Type constants
HttpMethodHTTP method constants (class từ Spring 6)
CacheControlCache-Control header builder
ContentDispositionContent-Disposition header
ResponseCookieSet-Cookie builder
ProblemDetailRFC 9457 (7807) error response
RestTemplateClassic blocking HTTP client
WebClientReactive/non-blocking HTTP client
RestClientModern blocking client (Spring 6.1+)

Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.