Skip to content
Chapter 21 · Spring Boot

ResponseEntity and HTTP Status Codes ​

Chapter 5 mapped requests to controller methods and used ResponseEntity briefly. Before the next chapter turns exceptions into proper error responses, this chapter covers responses in depth: how to build one with ResponseEntity, and which status code to return in which situation, category by category (1xx to 5xx).

An HTTP response has three parts:

  • Status code: the result, such as 200 OK or 500 Internal Server Error.
  • Headers: extra information (optional).
  • Body: the data you send back.

ResponseEntity ​

ResponseEntity<T> represents the whole response. T is the type of the body.

java
@GetMapping("/get-user")
public ResponseEntity<String> getUser() {
    return ResponseEntity.ok("My response body object can go here");
}

To set the status and headers yourself:

java
@GetMapping("/get-user")
public ResponseEntity<String> getUser() {
    HttpHeaders headers = new HttpHeaders();
    headers.add("My-Header1", "SomeValue1");
    headers.add("My-Header2", "SomeValue2");

    return ResponseEntity.status(HttpStatus.OK)
            .headers(headers)
            .body("My response body object can go here");
}

This is the builder pattern: status(...) and headers(...) return a builder, and body(...) returns the finished ResponseEntity, so body always comes last. When there is no body, finish with build() and use Void as the type:

java
@GetMapping("/get-user")
public ResponseEntity<Void> getUser() {
    return ResponseEntity.status(HttpStatus.OK).headers(headers).build();
}

The default: 200 OK ​

If a @RestController method returns a plain object, the status is 200 OK:

java
@RestController
@RequestMapping("/api")
public class UserController {

    @GetMapping("/get-user")
    public User getUser() {
        return new User("XYZ", 28);   // 200 OK, User as JSON in the body
    }
}

This works without @ResponseBody because @RestController adds it to every method. With @Controller instead, returning "XYZ" would make Spring look for a view named XYZ, and the request fails with 404 (chapter 5).

Status codes ​

The first digit gives the category.

1xx: Informational ​

An interim answer before the final one.

CodeNameUsed when
100ContinueBefore sending a large body (say a 1 MB file upload), the client sends only the headers, including Expect: 100-continue. The server checks what it can (authentication, authorization, content type, length) and, if all is fine, answers 100 Continue. The client then sends the body on the same request, and the server processes it.

2xx: Success ​

The request was received and processed.

CodeNameUsed mostly withMeaning
200OKGET, POSTSuccess, and the response has a body.
201CreatedPOSTSuccess, and a new resource was created.
202AcceptedPOSTAccepted, but processing is not finished yet, for example a batch export or import.
204No ContentDELETESuccess, and there is no body.
206Partial ContentGETThe server returns only part of a resource because the client asked for a byte range (a Range header), as when resuming a download.

You will see 206 used for bulk operations that partly succeeded (95 of 100 users added). That is not what the standard defines it for: for mixed results, return 200 (or 207 Multi-Status) with a per-item result in the body.

200 is also the right answer to a repeated, idempotent POST: if the same "create" request arrives twice and the second one creates nothing new, return 200 with the existing resource, and keep 201 for the call that really created it.

3xx: Redirection ​

The client must do something more to complete the request.

CodeNameMeaning
301Moved PermanentlyThe API moved for good, for example when migrating from a legacy API; clients should use the new URI from the Location header. Clients may change the method on redirect (a POST may be retried as GET).
308Permanent RedirectSame as 301, but the method must not change: if the old call was POST, the new one is POST too.
304Not ModifiedThe client cached an earlier GET response along with its Last-Modified time. On the next GET it sends that time in If-Modified-Since; if the resource has not changed, the server answers 304 with no body and the client uses its cache.

Try a 301. An old endpoint that has moved:

java
@GetMapping("/old-get-user")
public ResponseEntity<Void> oldGetUser() {
    return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY)
            .header("Location", "/api/new-get-user")
            .build();
}

@GetMapping("/new-get-user")
public ResponseEntity<String> newGetUser() {
    return ResponseEntity.status(HttpStatus.OK).body("success");
}

A client that does not follow redirects sees the bare answer:

http
HTTP/1.1 301
Location: /api/new-get-user
Content-Length: 0

A client that does (browsers, Postman by default, curl -L) reads Location, calls the new URI, and ends with 200 and success, as if it had called the new API directly. In Postman, turning off Settings → Automatically follow redirects shows the raw 301. That is the "additional action" of 3xx: the client, not the server, makes the second call.

304 is for caching GETs. If a PATCH changes nothing (the name is already the same in the database), answer 200 or 204, not 304.

4xx: Client errors ​

The client must send a correct request.

CodeNameExample
400Bad RequestThe client did not send the details needed to process the request.
401UnauthorizedThe API needs authentication (a Bearer token, Basic authentication, ...) and the client sent none.
403ForbiddenThe client is authenticated but not allowed: only ADMIN may do this operation and a non-admin called it.
404Not FoundThe resource is not there: GET user 123, but no such ID exists in the database.
405Method Not AllowedCalling a GET API with the POST method. In Spring Boot the DispatcherServlet rejects it before any controller runs, and adds an Allow header listing the methods that work (Allow: GET).
409ConflictThe request conflicts with the resource's current state. For example, a PATCH on user 123 is still in progress and an identical one arrives: the server keeps a lock per resource (often in a cache, with a short TTL so it frees itself) and answers the second with 409. Common with POST, PATCH and DELETE.
422Unprocessable EntityThe request is well formed but fails a business rule: users from a country you do not support yet cannot open an account.
429Too Many RequestsRate limiting: each user may make 10 calls a minute, and user 12345 makes an 11th.

5xx: Server errors ​

The client's request was valid, but something failed on the server.

CodeNameMeaning
500Internal Server ErrorGeneric error when no more specific code fits.
501Not ImplementedThe server cannot fulfil this request, for example the API is still being built.
502Bad GatewayA server acting as a proxy got an invalid response from upstream. Your app runs behind Nginx, and Nginx cannot reach it (say, because of a wrong port): Nginx returns 502.

Next: Status codes say what happened; most error codes come from exceptions. Chapter 22 follows an exception thrown by a controller through Spring, and shows how to turn it into the right status code and a useful error body.

Check yourself ​

  1. What are the three parts of an HTTP response?
  2. Why must .body(...) come last when building a ResponseEntity? What do you call instead when there is no body?
  3. What status does a @RestController return by default?
  4. A client uploads a large file and wants to know first whether the server will accept it. Which header and status code are involved?
  5. Which code tells the client "done, nothing to return"? And "accepted, still working on it"?
  6. What is the difference between 301 and 308?
  7. What is the difference between 401 and 403?
  8. The request is valid JSON but the country is not supported. Which code?
  9. Your app is down behind Nginx and Nginx cannot reach it. Which code does the client see?
Answers
  1. Status code, headers and body.
  2. status and headers return a builder; body returns the finished ResponseEntity. Use build() when there is no body.
  3. 200 OK.
  4. The client sends Expect: 100-continue; the server answers 100 Continue, then the client sends the body.
  5. 204 No Content. 202 Accepted.
  6. Both are permanent redirects; 308 forbids changing the HTTP method, 301 allows it.
  7. 401: not authenticated. 403: authenticated but not allowed.
  8. 422 Unprocessable Entity.
  9. 502 Bad Gateway.