Problem Details RFC 9457 — error response chuẩn hoá
RFC 9457 định nghĩa format JSON cho HTTP API error response với 5 trường chuẩn: type, title, status, detail, instance. Spring 6 có ProblemDetail native. Bài này giải thích tại sao cần chuẩn RFC, cách dùng ProblemDetail trong @RestControllerAdvice, nguyên tắc bảo mật không leak stack trace, và test exception handler.
TL;DR: RFC 9457 (kế thừa RFC 7807) là chuẩn HTTP API error response — định nghĩa 5 trường JSON: type, title, status, detail, instance. Spring 6 có ProblemDetail class native, Spring Boot 3 bật qua spring.mvc.problemdetails.enabled=true. Dùng @RestControllerAdvice trả ProblemDetail từ domain exception. Quy tắc bảo mật cứng: không bao giờ đưa stack trace vào response — log server-side, response chỉ trả requestId để support tra cứu.
Bài trước — Exception advice với @ControllerAdvice — đã chỉ cách Spring route exception qua 3 layer và đặt @RestControllerAdvice. Bài này tập trung vào shape của response: dùng format gì, trường nào bắt buộc, và tại sao chuẩn RFC 9457 thay thế Map<String, Object> ad-hoc cũ.
1. Tại sao cần chuẩn RFC 9457
Trước chuẩn này, mỗi team tự định nghĩa format error response riêng:
{ "error": "not_found", "msg": "Order 42 not found" }
{ "success": false, "message": "Order not found", "code": 404 }
{ "status": 404, "errorMessage": "Not found", "errorCode": "E004" }
Ba format khác nhau từ ba team khác nhau. Client (frontend, mobile, integration partner) phải viết parser riêng cho từng backend. Khi đổi team, field name thay đổi → client code phải sửa theo.
RFC 9457 (Request For Comments số 9457, IETF 2023) giải quyết vấn đề đó bằng cách định nghĩa một contract chung cho HTTP API error response. Lợi ích cụ thể:
- Client parse đồng nhất — biết field nào luôn có, field nào optional.
- Content-Type riêng biệt —
application/problem+json, client phân biệt được error response so với success response cùng kiểu JSON. - Extensible — custom field vẫn được thêm vào bên cạnh 5 trường chuẩn.
- Discoverable — trường
typetrỏ URI tới documentation giải thích error đó.
Khi toàn app (và toàn công ty) dùng cùng chuẩn, client chỉ cần 1 error handler dùng chung cho mọi endpoint.
flowchart LR
subgraph Before["Truoc RFC 9457"]
B1["API A<br/>error + msg"] -->|"parse rieng"| CL1["Client"]
B2["API B<br/>success=false + message"] -->|"parse rieng"| CL1
B3["API C<br/>status + errorMessage"] -->|"parse rieng"| CL1
end
subgraph After["Sau RFC 9457"]
A1["API A"] -->|"ProblemDetail"| Handler["1 error handler<br/>chung"]
A2["API B"] -->|"ProblemDetail"| Handler
A3["API C"] -->|"ProblemDetail"| Handler
end2. 5 trường chuẩn RFC 9457
RFC 9457 định nghĩa 5 trường trong object JSON error response:
| Trường | Bắt buộc | Kiểu | Mô tả |
|---|---|---|---|
type | Không | URI string | URI định danh loại lỗi. Nếu không set, mặc định "about:blank". Nên trỏ tới documentation page. |
title | Không | String | Tiêu đề ngắn gọn mô tả loại lỗi (1 dòng). Không phụ thuộc vào instance cụ thể. |
status | Bắt buộc | Integer | HTTP status code. Duplicate từ response header — tiện cho client đọc body mà không cần parse header. |
detail | Không | String | Chi tiết cụ thể cho instance lỗi này. Khác title ở chỗ phụ thuộc vào context request. |
instance | Không | URI string | URI identify instance lỗi, thường là request URL (ví dụ /api/v1/orders/42). |
Ngoài 5 trường chuẩn, spec cho phép thêm extension member bất kỳ — orderId, violations, requestId, timestamp, ... Đây là điểm mạnh của RFC 9457 so với format cứng nhắc.
Ví dụ response đầy đủ:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://api.olhub.org/errors/order-not-found",
"title": "Order not found",
"status": 404,
"detail": "Order 42 was not found or has been deleted",
"instance": "/api/v1/orders/42",
"orderId": 42,
"timestamp": "2026-06-09T08:00:00Z"
}
Ở đây orderId và timestamp là extension member — ngoài spec nhưng hoàn toàn hợp lệ theo RFC.
3. Spring 6 ProblemDetail — cơ chế bên dưới
ProblemDetail là class Java được Spring Framework 6.0 (Boot 3.0) đưa vào core. Cơ chế hoạt động cụ thể:
flowchart TB
EH["@ExceptionHandler<br/>return ProblemDetail"] --> JC["Jackson serialize<br/>ProblemDetail -> JSON"]
JC --> CT["Content-Type:<br/>application/problem+json"]
CT --> Resp["HTTP Response body"]Khi một method trong @RestControllerAdvice trả về ProblemDetail, Spring MVC thực hiện 3 bước:
- Jackson serialize object
ProblemDetailthành JSON — 5 trường chuẩn + mọi extension member được thêm quasetProperty. - Spring tự động set
Content-Type: application/problem+json(không phảiapplication/jsonthông thường). - HTTP status được lấy từ trường
statustrongProblemDetail.
Để built-in MVC exception (validation, type mismatch, ...) cũng trả ProblemDetail, cần bật property:
spring:
mvc:
problemdetails:
enabled: true
Spring Boot 3.4 trở lên bật mặc định cho project mới. Boot 3.0-3.3 phải set thủ công.
3.1 ProblemDetail API
// Tao ProblemDetail voi status code
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
// Tao voi status + detail message ngay
ProblemDetail pd = ProblemDetail.forStatusAndDetail(
HttpStatus.NOT_FOUND,
"Order 42 was not found"
);
// Set cac truong chuan
pd.setType(URI.create("https://api.olhub.org/errors/order-not-found"));
pd.setTitle("Order not found");
pd.setInstance(URI.create("/api/v1/orders/42"));
// Add extension member (custom field)
pd.setProperty("orderId", 42L);
pd.setProperty("timestamp", Instant.now());
pd.setProperty("retryAfter", 30);
setProperty(key, value) nhận Object — Jackson serialize bất kỳ kiểu Java nào thành JSON value tương ứng.
4. GlobalExceptionHandler trả ProblemDetail
Pattern production chuẩn: một @RestControllerAdvice tập trung map domain exception sang ProblemDetail:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// Domain exception: not found -> 404
@ExceptionHandler(OrderNotFoundException.class)
public ProblemDetail handleOrderNotFound(OrderNotFoundException ex,
HttpServletRequest req) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
pd.setType(URI.create("https://api.olhub.org/errors/order-not-found"));
pd.setTitle("Order not found");
pd.setDetail(ex.getMessage());
pd.setInstance(URI.create(req.getRequestURI()));
pd.setProperty("orderId", ex.getOrderId());
return pd;
}
// Conflict -> 409
@ExceptionHandler(DuplicateOrderException.class)
public ProblemDetail handleDuplicate(DuplicateOrderException ex,
HttpServletRequest req) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.CONFLICT);
pd.setTitle("Duplicate order");
pd.setDetail(ex.getMessage());
pd.setInstance(URI.create(req.getRequestURI()));
return pd;
}
// Validation -> 400 voi danh sach violations
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex,
HttpServletRequest req) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
pd.setType(URI.create("https://api.olhub.org/errors/validation"));
pd.setTitle("Validation failed");
pd.setDetail("Request has " + ex.getBindingResult().getErrorCount() + " invalid fields");
pd.setInstance(URI.create(req.getRequestURI()));
List<Map<String, Object>> violations = ex.getBindingResult().getFieldErrors().stream()
.map(err -> {
Map<String, Object> v = new LinkedHashMap<>();
v.put("field", err.getField());
v.put("message", err.getDefaultMessage());
v.put("rejectedValue", err.getRejectedValue());
return v;
})
.toList();
pd.setProperty("violations", violations);
return pd;
}
// Catch-all -> 500, KHONG leak detail
@ExceptionHandler(Exception.class)
public ProblemDetail handleAll(Exception ex, HttpServletRequest req) {
String requestId = MDC.get("requestId");
log.error("Unhandled exception, requestId={}, path={}",
requestId, req.getRequestURI(), ex); // log SERVER-side day du
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
pd.setTitle("Internal server error");
pd.setDetail("An unexpected error occurred. Contact support with the request ID.");
pd.setInstance(URI.create(req.getRequestURI()));
pd.setProperty("requestId", requestId); // client dung requestId de lien he support
return pd;
}
}
Lý do domain exception (OrderNotFoundException) không import org.springframework.http.*: domain layer thuần POJO, không phụ thuộc Spring hay HTTP. Tầng web (GlobalExceptionHandler) chịu trách nhiệm dịch sang HTTP semantics. Hệ quả: service layer test được với JUnit + Mockito đơn giản, không cần @SpringBootTest.
Liên hệ thêm về validation constraints trong extension member violations — xem Jakarta Bean Validation — constraints.
5. Bảo mật — tại sao không leak stack trace
Stack trace trong response body là lỗ hổng bảo mật nghiêm trọng. Cụ thể attacker thu thập được:
- Class path nội bộ —
com.olhub.order.OrderRepositorytiết lộ cấu trúc package và kiến trúc. - Phiên bản thư viện —
at org.hibernate.internal.SessionImpl.list(SessionImpl.java:760)cho biết Hibernate version cụ thể; attacker tra CVE database tìm exploit. - SQL query —
com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Table 'orders_db.user_email' doesn't existtiết lộ tên bảng, schema. - Config chi tiết — connection string, datasource URL đôi khi xuất hiện trong trace JDBC.
Pattern anti-pattern cần tránh tuyệt đối:
// ANTI-PATTERN: KHONG bao gio dung
@ExceptionHandler(Exception.class)
public ProblemDetail handleAll(Exception ex) {
ProblemDetail pd = ProblemDetail.forStatus(500);
pd.setDetail(ex.getMessage()); // co the chua PII / schema
pd.setProperty("stackTrace", ExceptionUtils.getStackTrace(ex)); // leak architecture
return pd;
}
Pattern an toàn — log full detail server-side, response chỉ trả requestId:
// DUNG: log server, response generic
@ExceptionHandler(Exception.class)
public ProblemDetail handleAll(Exception ex, HttpServletRequest req) {
String requestId = MDC.get("requestId");
// Full stack trace chi o server log, khong di vao response
log.error("Unhandled exception, requestId={}", requestId, ex);
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.INTERNAL_SERVER_ERROR);
pd.setTitle("Internal server error");
pd.setDetail("An unexpected error occurred. Contact support with the request ID.");
pd.setProperty("requestId", requestId);
return pd;
}
Quy trình hỗ trợ: user gặp 500 → đọc requestId trong response → báo support → support query Kibana/ELK với requestId → tìm được log đầy đủ + stack trace → fix. Client an toàn, support vẫn có đủ thông tin.
Config production vs development:
# application-prod.yml
server:
error:
include-stacktrace: never # PROD: tuyet doi khong expose
include-message: never
include-binding-errors: never
# application-dev.yml
server:
error:
include-stacktrace: on_param # dev: ?trace=true -> hien (chi noi bo)
include-message: always
include-binding-errors: always
6. Cơ chế bên dưới — ResponseEntityExceptionHandler
Khi bật spring.mvc.problemdetails.enabled=true, Spring MVC kích hoạt DefaultHandlerExceptionResolver tích hợp với ResponseEntityExceptionHandler. Class này là base class có sẵn mà bạn có thể extend để override xử lý exception built-in:
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// Override method co san trong base class
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
ProblemDetail pd = ex.getBody(); // lay ProblemDetail da co san
pd.setType(URI.create("https://api.olhub.org/errors/validation"));
List<Map<String, Object>> violations = ex.getBindingResult().getFieldErrors().stream()
.map(err -> Map.of(
"field", err.getField(),
"message", err.getDefaultMessage()
))
.toList();
pd.setProperty("violations", violations);
return ResponseEntity.status(status).body(pd);
}
// Them handler cho domain exception
@ExceptionHandler(OrderNotFoundException.class)
public ProblemDetail handleOrderNotFound(OrderNotFoundException ex,
HttpServletRequest req) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.NOT_FOUND);
pd.setTitle("Order not found");
pd.setDetail(ex.getMessage());
pd.setProperty("orderId", ex.getOrderId());
return pd;
}
}
Extend ResponseEntityExceptionHandler cho phép override từng built-in exception handler mà không cần viết lại logic Spring — chỉ thêm custom field. Không extend thì vẫn được, nhưng phải tự tạo ProblemDetail từ đầu thay vì dùng ex.getBody().
7. Test exception handler
Test exception handler với MockMvc để verify cả status code lẫn body shape:
@WebMvcTest(OrderController.class)
@Import(GlobalExceptionHandler.class)
class OrderExceptionHandlerTest {
@Autowired MockMvc mockMvc;
@MockitoBean OrderService orderService;
@Test
void orderNotFound_returns404WithProblemDetail() throws Exception {
when(orderService.findById(99L))
.thenThrow(new OrderNotFoundException(99L));
mockMvc.perform(get("/api/v1/orders/99"))
.andExpect(status().isNotFound())
.andExpect(content().contentType("application/problem+json"))
.andExpect(jsonPath("$.title").value("Order not found"))
.andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.detail").value("Order 99 was not found"))
.andExpect(jsonPath("$.instance").value("/api/v1/orders/99"))
.andExpect(jsonPath("$.orderId").value(99));
}
@Test
void serverError_doesNotLeakInternals() throws Exception {
when(orderService.findById(any()))
.thenThrow(new RuntimeException(
"DB error: user_email='[email protected]' schema=orders_db"));
String body = mockMvc.perform(get("/api/v1/orders/1"))
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.requestId").exists())
.andReturn().getResponse().getContentAsString();
// Verify khong leak thong tin noi bo
assertThat(body).doesNotContain("[email protected]");
assertThat(body).doesNotContain("orders_db");
assertThat(body).doesNotContain("stackTrace");
}
}
Test thứ hai kiểm tra trực tiếp rằng handler bắt exception chứa PII trong message nhưng response không lộ ra. Đây là test bảo mật nên có trong CI.
Pitfall của riêng concept này
Pitfall 1 — Quên enable Problem Details:
# THIEU: Spring Boot 3.0-3.3 can set thu cong
spring:
mvc:
problemdetails:
enabled: true
Nếu thiếu, built-in MVC exception trả Boot's legacy JSON format (timestamp, error, message, path) — không phải RFC 9457. Verify bằng /actuator/configprops.
Pitfall 2 — Dùng @ControllerAdvice thay @RestControllerAdvice cho REST API:
// SAI: method return ProblemDetail nhung KHONG co @ResponseBody
@ControllerAdvice
public class GlobalHandler {
@ExceptionHandler(OrderNotFoundException.class)
public ProblemDetail handle(OrderNotFoundException ex) { ... }
}
@ControllerAdvice không tự động serialize return value thành JSON body. Phải dùng @RestControllerAdvice (= @ControllerAdvice + @ResponseBody).
Pitfall 3 — Đặt type là URI không tồn tại:
{ "type": "https://api.olhub.org/errors/order-not-found" }
Nếu URL đó 404, client developer bấm vào không ra doc nào. RFC 9457 khuyến nghị type URI phải trỏ tới page có thật hoặc dùng about:blank nếu chưa có doc.
Pitfall 4 — Đưa ex.getMessage() trực tiếp vào detail cho generic Exception:
Exception message từ library thường chứa thông tin nhạy cảm. Chỉ an toàn dùng getMessage() cho domain exception do bạn viết và kiểm soát. Với catch-all Exception.class, dùng message generic cố định.
Liên hệ các bài khác
- Bài 01 — Exception advice với @ControllerAdvice: giải thích 3 layer exception handling và cách Spring route exception — bài này tập trung vào shape của response mà advice trả về. Đọc bài 01 trước để hiểu tại sao advice global tốt hơn handler local.
- Bài 03 — Jakarta Bean Validation: constraint validation tạo ra
MethodArgumentNotValidException— bài này chỉ cách format error response cho exception đó vớiviolationsarray. Bài 03 giải thích cách khai báo constraint trên DTO, bài này giải thích client nhận được gì khi constraint vi phạm. - Exception handling bài 05 module 01: bài tổng quan đầy đủ hơn về 3 layer exception + domain exception pattern + production monitoring. Bài hiện tại là phiên bản atomic tập trung vào RFC 9457.
Tóm tắt
- RFC 9457 (IETF 2023) chuẩn hoá HTTP API error response — 5 trường:
type,title,status,detail,instance. Extension member tự do. - Tại sao dùng: client parse đồng nhất,
Content-Type: application/problem+jsonphân biệt error vs success,typeURI trỏ tới doc. - Spring 6 có
ProblemDetailnative.setProperty(key, value)thêm extension member. Bậtspring.mvc.problemdetails.enabled=truecho built-in MVC exception. - Pattern:
@RestControllerAdvicetrảProblemDetail— domain exception pure (không import HTTP), web layer map sang HTTP semantics. - Bảo mật cứng: không đưa stack trace, exception message thô, schema, hay class path vào response. Log server-side với
requestId, response trảrequestIdcho support tra cứu. - Test:
@WebMvcTestverify status,Content-Type: application/problem+json, body shape RFC 9457, và không leak nội bộ.
Tự kiểm tra
Q1RFC 9457 định nghĩa 5 trường chuẩn cho HTTP API error response. Trường nào bắt buộc? Trường type mặc định là gì khi không set?▸
type mặc định là gì khi không set?Trong 5 trường, chỉ status là bắt buộc theo spec — phản ánh HTTP status code (integer). Bốn trường còn lại (type, title, detail, instance) là optional theo RFC.
Khi không set, Spring MVC điền type mặc định là "about:blank" — theo đúng RFC 9457 spec. Đây là giá trị sentinel nghĩa là "không có tài liệu cụ thể cho loại lỗi này".
Thực tế production nên set type trỏ tới documentation page thật (https://api.olhub.org/errors/order-not-found) để client developer bấm vào đọc được giải thích và cách fix. Public API như Stripe và GitHub đều có pattern này.
Extension member (custom field như orderId, violations, requestId) hoàn toàn hợp lệ — RFC 9457 cho phép thêm bất kỳ trường nào bên cạnh 5 trường chuẩn.
Q2Tại sao Spring dùng Content-Type: application/problem+json thay vì application/json cho error response? Điều này giúp gì cho client?▸
Content-Type: application/problem+json thay vì application/json cho error response? Điều này giúp gì cho client?application/problem+json là media type riêng biệt được đăng ký với IANA, định nghĩa trong RFC 9457. Nó khác application/json về mặt ngữ nghĩa: báo cho client biết body này là problem detail document — không phải resource bình thường.
Lợi ích cụ thể cho client:
- Phân biệt error vs success mà không cần parse body: HTTP client kiểm tra
Content-Typeheader → biết ngay đây là error document, không cần check$.successhay$.error. - Generic error handler: client viết một handler cho
application/problem+json, áp dụng cho mọi API tuân thủ RFC — kể cả API của vendor khác. - Tooling hỗ trợ: OpenAPI generator, mock framework, API gateway đều nhận ra media type này và có thể tự động generate code xử lý error.
Spring tự động set Content-Type này khi method trong @RestControllerAdvice trả về ProblemDetail — không cần khai báo thủ công.
Q3Giải thích tại sao đưa stack trace vào response body production là lỗ hổng bảo mật. Nêu ít nhất 3 loại thông tin cụ thể mà attacker có thể khai thác.▸
Stack trace chứa thông tin kỹ thuật chi tiết về bên trong hệ thống mà attacker có thể dùng để lập kế hoạch tấn công:
- Class path và kiến trúc nội bộ —
at com.olhub.order.repository.OrderRepository.findById(OrderRepository.java:42): tiết lộ tên package, cấu trúc module, class nào gọi class nào. Attacker hiểu được kiến trúc để tìm điểm yếu. - Phiên bản thư viện —
at org.hibernate.internal.SessionImpl.list(SessionImpl.java:760): Hibernate version cụ thể. Attacker tra CVE database tìm CVE với exploit công khai cho version đó. Library version không nên là public knowledge. - Database schema —
MySQLSyntaxErrorException: Table orders_db.user_profiles doesn't exist: tên database, tên bảng, đôi khi tên column. Attacker dùng thông tin này để xây dựng SQL injection payload chính xác hơn. - PII trong exception message —
Duplicate entry [email protected] for key email: email user, có thể cả dữ liệu từ request ban đầu, xuất hiện trong trace.
Pattern đúng: log toàn bộ stack trace ở server (Kibana, Loki), response chỉ trả requestId. Support tra cứu log với requestId khi cần debug. Client an toàn, support vẫn đủ thông tin.
Q4Khi nào nên extend ResponseEntityExceptionHandler? Lợi ích so với viết @RestControllerAdvice từ đầu không extend?▸
ResponseEntityExceptionHandler? Lợi ích so với viết @RestControllerAdvice từ đầu không extend?ResponseEntityExceptionHandler là base class của Spring MVC có sẵn handler cho ~20 built-in MVC exception (MethodArgumentNotValidException, HttpRequestMethodNotSupportedException, HttpMediaTypeNotSupportedException, ...). Mỗi method trong class đó trả ResponseEntity<Object> với body ProblemDetail đã điền sẵn status + title.
Nên extend khi muốn customize thêm vào handler built-in — ví dụ thêm trường violations vào MethodArgumentNotValidException mà vẫn giữ logic Spring:
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex, ...) {
ProblemDetail pd = ex.getBody(); // Spring da tao ProblemDetail co san
pd.setProperty("violations", buildViolations(ex));
return ResponseEntity.status(status).body(pd);
}Không extend vẫn được nếu chỉ cần handler cho domain exception và catch-all. Phải tự tạo ProblemDetail từ đầu cho built-in exception:
@ExceptionHandler(MethodArgumentNotValidException.class)
public ProblemDetail handleValidation(MethodArgumentNotValidException ex) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
// Tu set tat ca field
return pd;
}Trade-off: Extend tiết kiệm code vì dùng lại logic Spring, nhưng phải hiểu contract của base class (parameter type, return type). Không extend đơn giản hơn khi mới bắt đầu.
Q5Test sau chỉ assert status code. Thêm ít nhất 4 assertion cần thiết để test exception handler đúng cách theo RFC 9457.mockMvc.perform(get("/api/v1/orders/99"))
.andExpect(status().isNotFound());
▸
mockMvc.perform(get("/api/v1/orders/99"))
.andExpect(status().isNotFound());Test chỉ kiểm tra HTTP status — chưa đủ. Exception handler có thể trả status đúng nhưng body sai format, Content-Type sai, hoặc thiếu field RFC 9457.
4 assertion cần thêm tối thiểu:
mockMvc.perform(get("/api/v1/orders/99"))
.andExpect(status().isNotFound())
// 1. Content-Type phai la application/problem+json, khong phai application/json
.andExpect(content().contentType("application/problem+json"))
// 2. Cac truong chuan RFC 9457
.andExpect(jsonPath("$.status").value(404))
.andExpect(jsonPath("$.title").value("Order not found"))
.andExpect(jsonPath("$.detail").exists())
// 3. Instance URI - request URL
.andExpect(jsonPath("$.instance").value("/api/v1/orders/99"))
// 4. Extension member custom - dam bao handler add dung
.andExpect(jsonPath("$.orderId").value(99));Ngoài ra nên có test bảo mật riêng:
// Test catch-all khong leak PII
when(orderService.findById(any()))
.thenThrow(new RuntimeException("DB: user_email='[email protected]'"));
String body = mockMvc.perform(get("/api/v1/orders/1"))
.andExpect(status().isInternalServerError())
.andReturn().getResponse().getContentAsString();
assertThat(body).doesNotContain("[email protected]");
assertThat(body).doesNotContain("stackTrace");
assertThat(body).contains("requestId");Test exception path quan trọng ngang happy path. Handler bug khó phát hiện qua review — chỉ lộ khi production gặp edge case lúc 2 giờ sáng.
Bài tiếp theo: Jakarta Bean Validation — constraints
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên