Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Exception mappers

Checking "does the post exist" in every resource method is tedious, and a 404 response without a body tells the client little. It is more convenient to throw your own exception deep in the code and turn it into an HTTP response in one place. Jakarta REST has the ExceptionMapper for this — a class that receives an exception and returns a Response.

Your own exception

Create the file src/main/java/com/example/blog/PostNotFoundException.java:

// src/main/java/com/example/blog/PostNotFoundException.java
package com.example.blog;
 
public class PostNotFoundException extends RuntimeException {
 
    public PostNotFoundException(long id) {
        super("Post " + id + " not found");
    }
}

And a record for the error response body:

// src/main/java/com/example/blog/ErrorMessage.java
package com.example.blog;
 
public record ErrorMessage(int status, String error) {
}

The mapper

// src/main/java/com/example/blog/PostNotFoundMapper.java
package com.example.blog;
 
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
 
@Provider
public class PostNotFoundMapper implements ExceptionMapper<PostNotFoundException> {
 
    @Override
    public Response toResponse(PostNotFoundException e) {
        return Response.status(Response.Status.NOT_FOUND)
                .type(MediaType.APPLICATION_JSON)
                .entity(new ErrorMessage(404, e.getMessage()))
                .build();
    }
}

The @Provider annotation registers the class with Jakarta REST. The type in angle brackets, ExceptionMapper<PostNotFoundException>, says which exceptions it handles. When a resource method throws such an exception, the server calls toResponse and sends its result.

The store throws the exception

Add a get method to PostStore and change delete:

// src/main/java/com/example/blog/PostStore.java (fragment)
public Post get(long id) {
    return findById(id).orElseThrow(() -> new PostNotFoundException(id));
}
 
public void delete(long id) {
    if (posts.remove(id) == null) {
        throw new PostNotFoundException(id);
    }
}

The resource methods become short — no more checks in them:

// src/main/java/com/example/blog/PostResource.java (fragment)
@GET
@Path("/{id}")
public Post show(@PathParam("id") long id) {
    return store.get(id);
}
 
@DELETE
@Path("/{id}")
public void delete(@PathParam("id") long id) {
    store.delete(id);
}

Trying it out

curl -i http://localhost:8080/blog/api/posts/99
HTTP/1.1 404 Not Found
Content-Type: application/json
Content-Length: 42

{"error":"Post 99 not found","status":404}

Deleting a missing post (curl -i -X DELETE http://localhost:8080/blog/api/posts/42) returns the same response with the number 42.

Which mapper is chosen

SituationWhat happens
There is a mapper for this exception classIt is called
There is only a mapper for a parent, for example RuntimeExceptionThe parent's mapper is called
No mappersStatus 500, the exception goes to the log

A mapper for Exception catches everything — it is sometimes added to hide internal messages from clients. But then it also intercepts the NotFoundException from the previous lesson, so inside it you have to handle WebApplicationException separately.

Common mistake

Forgetting the @Provider annotation. The class compiles, but the server does not know about it, and the exception reaches the client as a 500 error with the message text:

HTTP/1.1 500 Internal Server Error

Post 99 not found

The server log shows RESTEASY002375: Error processing request GET /blog/api/posts/99 and the PostNotFoundException stack trace.

Thanks for reading!

Donate to the site — we work hard for you.