ResponseEntity & HTTP status — trả response đúng ngữ nghĩa REST
Ba cách trả response trong Spring MVC, ngữ nghĩa 5 nhóm status code HTTP (2xx/3xx/4xx/5xx), ResponseEntity builder pattern, và cơ chế HttpMessageConverter chuyển Java object thành JSON byte. Tại sao status code không phải decoration mà là REST contract.
TL;DR: Spring MVC có 3 cách trả response — return DTO trực tiếp (default 200 OK), ResponseEntity builder (custom status + header + body), và @ResponseStatus trên method. HTTP status code có ngữ nghĩa cụ thể theo từng nhóm: POST tạo resource mới nên trả 201 Created với Location header, DELETE trả 204 No Content, async operation trả 202 Accepted. ResponseEntity.created(uri).body(dto) là pattern chuẩn cho POST. Bên dưới, Spring dùng HttpMessageConverter — cụ thể là MappingJackson2HttpMessageConverter — để serialize Java object thành JSON bytes. Status code sai = phá REST contract với client.
Bài ArgumentResolver đã bóc cách Spring bind request vào tham số method. Bài này đi chiều ngược lại: khi controller return orderDto, Spring chọn status code nào? Header gì? Serialize body ra sao? Khi nào cần ResponseEntity thay vì return DTO trực tiếp?
1. Ba cách trả response
Spring MVC hỗ trợ 3 cách đặt response từ handler method, mỗi cách phù hợp một use case khác nhau.
1.1 Return DTO trực tiếp — default 200 OK
@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable Long id) {
return orderService.findById(id);
// Spring tu dong: status 200, body = JSON cua OrderDto
}
Đây là cách đơn giản nhất. Spring đặt status 200 OK mặc định và uỷ cho HttpMessageConverter serialize return value thành JSON. Không cần viết thêm gì khi endpoint luôn thành công và trả đúng 200.
1.2 ResponseEntity — kiểm soát hoàn toàn
@PostMapping("/orders")
public ResponseEntity<OrderDto> create(@Valid @RequestBody OrderRequest req) {
OrderDto created = orderService.create(req);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(created.id()).toUri();
return ResponseEntity.created(location).body(created);
// 201 Created + Location header + body
}
ResponseEntity<T> là wrapper giữ ba thứ cùng nhau: status code, headers, và body. Dùng khi cần status khác 200, custom header, hoặc body tuỳ điều kiện (có thể 200 hoặc 404 tuỳ logic).
1.3 @ResponseStatus — khai báo tĩnh
@DeleteMapping("/orders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
orderService.delete(id);
// 204 No Content, no body
}
@ResponseStatus khai báo status code cố định cho method, không cần bọc ResponseEntity. Phù hợp khi status luôn giống nhau và không cần custom header.
Bảng so sánh:
| Cách | Status | Header | Body tuỳ logic | Khi dùng |
|---|---|---|---|---|
| Return DTO | 200 cố định | Không custom | Không | GET đơn giản, luôn 200 |
ResponseEntity | Bất kỳ | Custom được | Có | POST 201, conditional 304/404, custom header |
@ResponseStatus | Khai báo cố định | Không custom | Không | DELETE 204, action không trả data |
2. HTTP status code — ngữ nghĩa 4 nhóm
HTTP status code (RFC 9110) không phải số tùy ý — chúng chia thành 4 nhóm với ngữ nghĩa riêng biệt. Chọn sai nhóm là phá vỡ REST contract: client không biết request thành công hay thất bại mà không parse body.
flowchart LR
subgraph G2["2xx -- Thanh cong"]
direction TB
A1["200 OK<br/>GET/PUT co body"]
A2["201 Created<br/>POST tao moi + Location"]
A3["202 Accepted<br/>async, chua hoan thanh"]
A4["204 No Content<br/>DELETE, khong body"]
end
subgraph G3["3xx -- Chuyen huong"]
direction TB
B1["301 Moved Permanently"]
B2["304 Not Modified<br/>cache hit, no body"]
end
subgraph G4["4xx -- Loi client"]
direction TB
C1["400 Bad Request<br/>validation fail"]
C2["401 Unauthorized<br/>chua xac thuc"]
C3["403 Forbidden<br/>khong quyen"]
C4["404 Not Found"]
C5["409 Conflict<br/>duplicate/state"]
C6["429 Too Many Requests"]
end
subgraph G5["5xx -- Loi server"]
direction TB
D1["500 Internal Server Error"]
D2["502 Bad Gateway"]
D3["503 Service Unavailable"]
end2.1 2xx — Thành công
200 OK là default khi return DTO trực tiếp. Nhưng POST tạo resource mới phải dùng 201 Created:
// Dung -- POST tra 201 Created
@PostMapping("/orders")
public ResponseEntity<OrderDto> create(@Valid @RequestBody OrderRequest req) {
OrderDto created = orderService.create(req);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(created.id()).toUri();
return ResponseEntity.created(location).body(created);
}
// Response headers:
// HTTP/1.1 201 Created
// Location: /api/orders/42
Vì sao 201 + Location quan trọng hơn 200? Vì nó nói cho client biết resource mới nằm ở đâu mà không cần client tự suy URL. Frontend có thể dùng Location header để redirect hoặc refetch mà không hard-code URL pattern.
204 No Content cho DELETE — action thành công nhưng không có gì để trả:
@DeleteMapping("/orders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
orderService.delete(id);
}
// HTTP/1.1 204 No Content (no body)
202 Accepted khi operation được nhận nhưng chưa hoàn thành — ví dụ async export job:
@PostMapping("/orders/{id}/export")
public ResponseEntity<Void> exportAsync(@PathVariable Long id) {
String jobId = exportService.startExportJob(id);
return ResponseEntity.accepted()
.header("X-Job-Id", jobId)
.build();
}
// HTTP/1.1 202 Accepted
// X-Job-Id: job-abc123
202 nói với client: "Tôi đã nhận yêu cầu, đang xử lý — poll GET /jobs/{jobId} sau để biết kết quả." Pattern này tránh timeout cho operation chạy vài phút.
2.2 3xx — Chuyển hướng
REST API hiếm dùng 3xx, ngoại trừ 304 Not Modified cho conditional GET:
@GetMapping("/orders/{id}")
public ResponseEntity<OrderDto> get(
@PathVariable Long id,
@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch
) {
OrderDto order = orderService.findById(id);
String etag = "\"" + order.version() + "\"";
if (etag.equals(ifNoneMatch)) {
return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
}
return ResponseEntity.ok().eTag(etag).body(order);
}
Client gửi If-None-Match: "v3" — nếu version chưa đổi, server trả 304 không có body, tiết kiệm bandwidth.
2.3 4xx — Lỗi do client
4xx nghĩa là client gửi sai — request thiếu thông tin, sai định dạng, hoặc không có quyền. Server không sai.
| Code | Tên | Khi dùng |
|---|---|---|
| 400 | Bad Request | Validation fail, JSON malformed, tham số thiếu |
| 401 | Unauthorized | Chưa xác thực (thiếu/sai token) |
| 403 | Forbidden | Đã xác thực nhưng không có quyền |
| 404 | Not Found | Resource không tồn tại |
| 409 | Conflict | Duplicate, state conflict (vd tạo user đã tồn tại) |
| 422 | Unprocessable Entity | Cú pháp đúng nhưng sai semantic (vd ngày kết thúc trước ngày bắt đầu) |
| 429 | Too Many Requests | Vượt rate limit |
Pitfall quan trọng nhất: không bao giờ trả 200 với body {"error": "..."} cho client error. Client dựa vào status code để quyết định xử lý UI — nếu status luôn 200, client phải parse body mới biết thất bại hay không. Đây là vi phạm REST contract.
2.4 5xx — Lỗi server
5xx nghĩa là server sai — client gửi đúng nhưng server không xử lý được.
| Code | Tên | Khi dùng |
|---|---|---|
| 500 | Internal Server Error | Unhandled exception trong server |
| 502 | Bad Gateway | Upstream service fail |
| 503 | Service Unavailable | Overloaded hoặc maintenance |
| 504 | Gateway Timeout | Upstream không phản hồi kịp |
5xx không bao giờ được trả cho request client gửi sai — đó là 4xx.
3. ResponseEntity builder — API đầy đủ
ResponseEntity dùng builder pattern — chain method để set từng phần response:
// Static factory nhanh
ResponseEntity.ok(body) // 200 + body
ResponseEntity.ok() // 200 builder, chain header roi .body(dto)
ResponseEntity.created(uri) // 201 + Location header tu uri
ResponseEntity.accepted() // 202
ResponseEntity.noContent() // 204 builder
ResponseEntity.notFound() // 404 builder
ResponseEntity.badRequest() // 400 builder
ResponseEntity.status(HttpStatus.CONFLICT) // bat ky status tuy chinh
// Chain header + body
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(total))
.header("X-Request-Id", requestId)
.lastModified(order.updatedAt())
.body(orders);
Generic type parameter:
ResponseEntity<OrderDto> // body kieu OrderDto
ResponseEntity<List<OrderDto>>
ResponseEntity<Void> // khong co body (DELETE, 202)
ResponseEntity<?> // kieu dong (su dung han che -- mat type safety)
ResponseEntity<Void> dùng cho response không body — phân biệt rõ ràng với ResponseEntity<OrderDto> trả về null body.
Pattern conditional response — cùng method có thể trả 200 hoặc 404:
@GetMapping("/orders/{id}")
public ResponseEntity<OrderDto> get(@PathVariable Long id) {
return orderService.findOptional(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
Method return type ResponseEntity<OrderDto> nên compiler kiểm tra được kiểu body.
4. Cơ chế bên dưới — HttpMessageConverter serialize
Khi Spring nhận return value từ handler method, nó không tự biết "phải ghi gì vào response". Cơ chế chịu trách nhiệm là HttpMessageConverter — interface quyết định serialize Java object thành bytes và ngược lại.
flowchart TB
A["Controller<br/>return OrderDto hoac ResponseEntity"] --> B["HandlerMethodReturnValueHandler<br/>xu ly return value"]
B --> C{"Content negotiation<br/>Accept header tu client?"}
C -->|"Accept: application/json"| D["MappingJackson2HttpMessageConverter<br/>Jackson serialize -> JSON bytes"]
C -->|"Accept: application/xml"| E["Jaxb2RootElementHttpMessageConverter<br/>JAXB serialize -> XML bytes"]
C -->|"Khong converter nao khop"| F["406 Not Acceptable"]
D --> G["HttpServletResponse<br/>status + headers + body bytes"]
E --> G
B -->|"ResponseEntity"| H["Lay status + headers tu ResponseEntity<br/>roi dua body qua converter"]
H --> DLuồng cụ thể khi return orderDto:
DispatcherServletgọiHandlerMethodReturnValueHandlerxử lý return value.- Handler xem
Acceptheader từ client (application/jsonlà default của hầu hết REST client). - Tìm
HttpMessageConverternào hỗ trợ kiểu Java (OrderDto) vàMediaType(application/json). MappingJackson2HttpMessageConverter(Jackson 2) được chọn — gọiObjectMapper.writeValue()serializeOrderDtothành JSON bytes.- Spring ghi bytes vào
HttpServletResponse.getOutputStream(), setContent-Type: application/json.
Khi return ResponseEntity, Spring trước tiên lấy status code và headers từ ResponseEntity, sau đó đưa body qua cùng pipeline converter trên.
Vì sao quan trọng: nếu Jackson không tìm thấy getter cho một field (ví dụ thiếu @JsonProperty), field đó không xuất hiện trong JSON — không có lỗi compile, chỉ là JSON bị thiếu field. Hiểu converter pipeline giúp debug loại bug này.
Content negotiation — Spring chọn converter dựa trên Accept header. Nếu client gửi Accept: application/xml nhưng không có Jackson XML hay JAXB trên classpath, Spring trả 406 Not Acceptable. Đây là tính năng, không phải bug: client khai báo muốn format gì, server chọn converter phù hợp.
5. Header phổ biến trong REST response
ResponseEntity cho phép set bất kỳ header nào. Các header có ngữ nghĩa quan trọng:
| Header | Ý nghĩa | Ví dụ giá trị |
|---|---|---|
Location | URL resource mới (bắt buộc với 201 Created) | /api/orders/42 |
ETag | Cache validator — version của resource | "v5" |
Last-Modified | Thời gian sửa lần cuối (cache) | Wed, 09 Jun 2026 10:00:00 GMT |
Cache-Control | Directive caching | max-age=300, public |
Content-Disposition | File download | attachment; filename="invoice.pdf" |
X-Total-Count | Tổng số phần tử cho pagination | 247 |
X-Request-Id | Correlation ID để trace request | req-abc123 |
Retry-After | Chờ bao lâu trước khi retry (429/503) | 60 |
// Pagination response voi header
@GetMapping
public ResponseEntity<List<OrderDto>> list(Pageable pageable) {
Page<OrderDto> page = orderService.list(pageable);
return ResponseEntity.ok()
.header("X-Total-Count", String.valueOf(page.getTotalElements()))
.header("X-Total-Pages", String.valueOf(page.getTotalPages()))
.header("X-Page-Number", String.valueOf(page.getNumber()))
.body(page.getContent());
}
6. Pitfall — những lỗi hay gặp
Pitfall 1 — POST trả 200 thay 201:
// SAI: POST tao moi nhung tra 200, khong co Location
@PostMapping("/orders")
public OrderDto create(@Valid @RequestBody OrderRequest req) {
return orderService.create(req);
}
// DUNG: 201 Created + Location header
@PostMapping("/orders")
public ResponseEntity<OrderDto> create(@Valid @RequestBody OrderRequest req) {
OrderDto created = orderService.create(req);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(created.id()).toUri();
return ResponseEntity.created(location).body(created);
}
Client (đặc biệt là API gateway hoặc SDK tự động) dùng Location header để biết URL resource vừa tạo. Thiếu Location là bỏ qua một phần của HTTP 201 spec (RFC 9110 §15.3.2).
Pitfall 2 — DELETE trả 200 với body null:
// SAI: void method, Spring gui 200 + body rong -- client confused
@DeleteMapping("/orders/{id}")
public void delete(@PathVariable Long id) {
orderService.delete(id);
}
// DUNG: 204 No Content, ro rang khong co body
@DeleteMapping("/orders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
orderService.delete(id);
}
Pitfall 3 — Trả 200 với body error cho 4xx:
// SAI: 200 OK nhung body bao loi -- pha REST contract
@GetMapping("/orders/{id}")
public Map<String, Object> get(@PathVariable Long id) {
if (!orderService.exists(id)) {
return Map.of("error", "not found"); // 200 voi body loi!
}
return Map.of("data", orderService.findById(id));
}
// DUNG: 404 ro rang
@GetMapping("/orders/{id}")
public ResponseEntity<OrderDto> get(@PathVariable Long id) {
return orderService.findOptional(id)
.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}
Frontend và mobile client đọc status code trước khi quyết định parse body. Nếu status luôn 200, client phải parse body để biết lỗi — vừa phức tạp vừa không nhất quán với các API khác.
Pitfall 4 — Dùng ResponseEntity khi không cần:
// THUA: chi GET don gian ma dung ResponseEntity
@GetMapping("/orders")
public ResponseEntity<List<OrderDto>> list() {
return ResponseEntity.ok(orderService.findAll());
}
// GIN GON: khi khong can custom gi
@GetMapping("/orders")
public List<OrderDto> list() {
return orderService.findAll();
}
Chỉ dùng ResponseEntity khi thực sự cần custom status code, header, hoặc body điều kiện.
Liên hệ các bài khác
- Bài 02 — ArgumentResolver: chiều ngược lại — Spring bind request (path variable, request body, header) vào tham số method. Hiểu cả hai chiều (argument resolution + return value handling) mới thấy toàn bộ request-response lifecycle trong DispatcherServlet.
- Bài 04 — Response advanced: đào sâu
ResponseBodyAdvice(global response wrapping),StreamingResponseBody(export file lớn không OOM),SseEmitter(real-time push), và HATEOAS link relation — những pattern build trên nềnResponseEntitybài này đặt ra. - Bài 05 — Exception handling: khi service throw exception,
@ExceptionHandler+@ControllerAdvicemap exception thành 4xx/5xx response chuẩn RFC 9457 Problem Details — cơ chế hoàn thiện bức tranh status code bài này bắt đầu.
Tóm tắt
- Spring có 3 cách trả response: return DTO trực tiếp (200 OK mặc định),
ResponseEntity(custom status + header + body),@ResponseStatus(status cố định trên method). - HTTP status code có ngữ nghĩa: POST mới → 201 Created +
Location; DELETE → 204 No Content; async → 202 Accepted; 4xx client sai; 5xx server sai. Không bao giờ trả 200 cho error case. ResponseEntitybuilder: chain.ok(),.created(uri),.accepted(),.noContent(),.notFound(),.status(...),.header(key, value),.body(dto).HttpMessageConverter— cụ thểMappingJackson2HttpMessageConverter— serialize Java object thành JSON bytes dựa trênAcceptheader. Đây là cơ chế bên dưới mọireturn dtotrong controller.- Content negotiation: Spring chọn converter dựa trên
Acceptheader. Không tìm được converter → 406 Not Acceptable. - Dùng
ResponseEntityđúng chỗ: chỉ khi cần custom status, custom header, hoặc body điều kiện. GET đơn giản trả 200 thì return DTO trực tiếp.
Tự kiểm tra
Q1Đoạn code dưới đây có 2 vấn đề về status code. Chỉ ra và viết lại đúng.@PostMapping("/products")
public ProductDto create(@Valid @RequestBody ProductRequest req) {
return productService.create(req);
}
@DeleteMapping("/products/{id}")
public ProductDto delete(@PathVariable Long id) {
productService.delete(id);
return null;
}
▸
@PostMapping("/products")
public ProductDto create(@Valid @RequestBody ProductRequest req) {
return productService.create(req);
}
@DeleteMapping("/products/{id}")
public ProductDto delete(@PathVariable Long id) {
productService.delete(id);
return null;
}Vấn đề 1 — POST trả 200 OK thay vì 201 Created, thiếu Location header:
POST tạo resource mới phải trả 201 Created kèm header Location trỏ đến URL của resource vừa tạo (theo RFC 9110 §15.3.2). Trả 200 là đúng về kỹ thuật nhưng sai về ngữ nghĩa — client không biết resource mới ở đâu mà không parse body và tự suy URL.
@PostMapping("/products")
public ResponseEntity<ProductDto> create(@Valid @RequestBody ProductRequest req) {
ProductDto created = productService.create(req);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}").buildAndExpand(created.id()).toUri();
return ResponseEntity.created(location).body(created);
}Vấn đề 2 — DELETE trả 200 với body null thay vì 204 No Content:
DELETE thành công không có gì để trả về. 204 No Content nói rõ ràng "thành công, không có body" — client không phải đoán body null là bình thường hay lỗi. Return void kết hợp với @ResponseStatus(HttpStatus.NO_CONTENT) là pattern chuẩn.
@DeleteMapping("/products/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
productService.delete(id);
}Q2Tại sao Spring chọn MappingJackson2HttpMessageConverter để serialize response thay vì các converter khác? Cơ chế nào quyết định converter nào được dùng?▸
MappingJackson2HttpMessageConverter để serialize response thay vì các converter khác? Cơ chế nào quyết định converter nào được dùng?Spring không hard-code một converter duy nhất — nó dùng content negotiation để chọn converter phù hợp dựa trên hai yếu tố: kiểu Java của return value và Accept header từ client.
Khi controller return OrderDto và client gửi Accept: application/json (hoặc không gửi Accept — mặc định JSON với REST client), Spring duyệt danh sách HttpMessageConverter đã đăng ký theo thứ tự ưu tiên. MappingJackson2HttpMessageConverter được đăng ký sẵn bởi Spring Boot Auto-configuration (có Jackson trên classpath) và hỗ trợ serialize bất kỳ Java object nào sang application/json.
Nếu client gửi Accept: application/xml, Spring tìm converter hỗ trợ XML — nếu không có JAXB hay Jackson XML trên classpath, Spring trả 406 Not Acceptable. Đây là content negotiation: server chỉ trả format mình hỗ trợ và client yêu cầu.
Hệ quả thực tế: nếu một field của OrderDto bị Jackson bỏ qua (thiếu getter, annotated @JsonIgnore, hoặc visibility config), field đó âm thầm mất khỏi JSON — không lỗi compile, không exception. Hiểu cơ chế converter giải thích tại sao.
Q3Bạn có endpoint GET /orders/{id}. Viết code xử lý 3 trường hợp: tìm thấy (200), không tìm thấy (404), và version chưa thay đổi (304 với ETag). Dùng ResponseEntity.▸
GET /orders/{id}. Viết code xử lý 3 trường hợp: tìm thấy (200), không tìm thấy (404), và version chưa thay đổi (304 với ETag). Dùng ResponseEntity.Ba trường hợp có thể xử lý trong cùng một method:
@GetMapping("/orders/{id}")
public ResponseEntity<OrderDto> get(
@PathVariable Long id,
@RequestHeader(value = "If-None-Match", required = false) String ifNoneMatch
) {
return orderService.findOptional(id)
.map(order -> {
String etag = "\"" + order.version() + "\"";
// 304: version chua thay doi, khong can gui lai body
if (etag.equals(ifNoneMatch)) {
return ResponseEntity.<OrderDto>status(HttpStatus.NOT_MODIFIED).build();
}
// 200: co body + ETag de client cache
return ResponseEntity.ok()
.eTag(etag)
.body(order);
})
// 404: khong tim thay
.orElseGet(() -> ResponseEntity.notFound().build());
}Luồng hoạt động:
Lần đầu client gọi: server trả 200 + body + header ETag: "v1". Client cache body và ETag. Lần hai client gửi If-None-Match: "v1": nếu order chưa cập nhật, version vẫn là 1, server trả 304 không có body — client dùng cached body, tiết kiệm bandwidth. Nếu order đã cập nhật lên version 2, ETag khác, server trả 200 với body mới.
Lưu ý ResponseEntity.<OrderDto>status(HttpStatus.NOT_MODIFIED) cần explicit type parameter vì không có .body() để compiler suy ra kiểu.
Q4Khi nào nên dùng @ResponseStatus trên method thay vì ResponseEntity? Cho 2 ví dụ cụ thể và giải thích tradeoff.▸
@ResponseStatus trên method thay vì ResponseEntity? Cho 2 ví dụ cụ thể và giải thích tradeoff.@ResponseStatus phù hợp khi status code **cố định và không thay đổi** theo logic — annotation khai báo một lần, không cần bọc return value trong ResponseEntity.
Ví dụ 1 — DELETE luôn trả 204:
@DeleteMapping("/orders/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void delete(@PathVariable Long id) {
orderService.delete(id);
}
// Ngan hon: khong can ResponseEntity<Void>, khong can .build()Ví dụ 2 — Action endpoint trả 202:
@PostMapping("/orders/{id}/archive")
@ResponseStatus(HttpStatus.ACCEPTED)
public void archiveAsync(@PathVariable Long id) {
archiveService.scheduleArchive(id);
// 202 Accepted -- viec nay chay async, khong tra ket qua ngay
}Tradeoff:
@ResponseStatus ngắn gọn hơn nhưng không thể custom header. Nếu DELETE cần trả header X-Deleted-At, hay POST async cần trả X-Job-Id, phải dùng ResponseEntity. @ResponseStatus cũng không cho phép status tuỳ điều kiện — nếu method có thể trả 200 hoặc 204 tùy input, dùng ResponseEntity.
Quy tắc đơn giản: dùng @ResponseStatus cho action/void method với status cố định. Dùng ResponseEntity khi cần header, body điều kiện, hoặc status động.
Q5Tại sao không bao giờ được trả 200 OK với body {"error": "not found"} cho trường hợp resource không tồn tại? Giải thích hậu quả cụ thể với client.▸
{"error": "not found"} cho trường hợp resource không tồn tại? Giải thích hậu quả cụ thể với client.HTTP status code là phần của REST contract — nó là tín hiệu máy đọc được, phân biệt thành công hay thất bại mà không cần parse body. Trả 200 với body error phá vỡ contract này theo ba cách cụ thể:
1. Client không biết request thất bại: JavaScript fetch(), Axios, Retrofit đều kiểm tra status code trước. Axios tự động throw error cho 4xx/5xx. Nếu status là 200, Axios không throw — code frontend không vào catch block, tưởng request thành công rồi cố render data.name từ body error → crash hoặc hiển thị sai.
2. Cache và proxy bị ảnh hưởng: CDN, browser cache, và HTTP proxy cache response 200 OK theo Cache-Control. Nếu response lỗi được cache, request sau trả lại cùng body lỗi đó dù resource đã được tạo.
3. Logging và monitoring sai: APM tool (Datadog, New Relic, Sentry) theo dõi error rate dựa trên status code. 200 với body error không được đếm là error — incident bị bỏ qua.
Cách đúng: dùng 404 Not Found với body RFC 9457 Problem Details:
// HTTP/1.1 404 Not Found
// Content-Type: application/problem+json
{
"type": "https://api.example.com/errors/not-found",
"title": "Order not found",
"status": 404,
"detail": "Order with id 42 does not exist",
"instance": "/api/orders/42"
}Client đọc status 404 → biết ngay không tìm thấy, không cần parse body. Body Problem Details chỉ để hiển thị thông báo cho người dùng.
Bài tiếp theo: Streaming, ResponseBodyAdvice và HATEOAS
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