Handling Exceptions and Error responses in Java Rest Services

Leejjon
15 min readApr 10, 2022

I often see presentations about best practices to detect bugs early on and prevent them. Obviously preventing is better than fixing. However, things can always go wrong. Even if your code is perfect the following things can happen:

  • The client can do invalid requests
  • Network problems
  • Hardware failures
  • The database might have incorrect data

In this post I will talk about handling errors. Handling errors nicely will inform users that something went wrong and that they should try again or wait until things have been fixed. If the errors are properly logged the developers can investigate and fix the problems faster.

Error handling in Java

If you know a bit of Java you’ll probably think of try-catch statements to handle exceptions. It’s important to know that Java has Checked and Unchecked exceptions. You can read more about Java Exceptions in the Oracle docs on Exceptions.

Here is a bit of Java code where I run two methods from the main thread. One can possibly throw a checked SQLException, while the other can throw a UncheckedException (in this code, the chance for it to happen is 50%):

import java.sql.SQLException;
import java.util.Random;

public class Main {
public static void main(String[] args) {
try {
checkedException();
unCheckedException();
} catch (SQLException | NullPointerException e) {
e.printStackTrace();
}
}

private static void checkedException() throws SQLException {
if (new Random().nextBoolean()) {
throw new SQLException();
}
}

private static void unCheckedException() {
if (new Random().nextBoolean()) {
throw new NullPointerException();
}
}
}

As you can see the checked exception needs a throws declaration, while the unchecked exception doesn’t. I used to like checked exceptions when I started to learn Java as it gave me information on what could go wrong in my code. While there are situations where this is useful, most Java REST services nowadays only use unchecked exceptions. I hope to make clear why in the next section.

Catching errors at the system boundaries

It’s only useful to catch an Exception in a method if the method can still fulfill its purpose. Let’s say we have a system like the design below:

The database service tries to execute an SQL query but it fails. This happens a lot in the real world. The query could have a syntax error, or attempt to retrieve data from a table that no longer exists.

We could put a try catch statement in the database service, but what needs to happen to nicely handle this error? Probably the controller needs to decide to send an HTTP 500 response. The database service is not able to do that, and neither does the business logic. So let’s put the try catch statement in the controller.

Here is how that looks in some simplified example code:

import java.sql.SQLException;
import java.util.Random;

public class Main {
public static void main(String[] args) {
BusinessLogic businessLogic = new BusinessLogic(new DatabaseService());
Controller controller = new Controller(businessLogic);
controller.callApi();
}

static class Controller {
private final BusinessLogic businessLogic;
public Controller(BusinessLogic businessLogic) {
this.businessLogic = businessLogic;
}

public String callApi() {
try {
businessLogic.doBusinessLogic();
return "200";
} catch (Exception e) {
e.printStackTrace();
return "500";
}
}
}

static class BusinessLogic {
private final DatabaseService databaseService;
public BusinessLogic(DatabaseService databaseService) {
this.databaseService = databaseService;
}

public void doBusinessLogic() throws SQLException {
databaseService.executeQuery(true);
}
}

static class DatabaseService {
public void runQuery(boolean extraParam)
throws SQLException {
Connection con = DriverManager.getConnection(
"jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1"
); // Some simple in memory db
String sql = "select * from records where extraParam = ?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, Boolean.toString(extraParam));
ps.executeQuery();
con.close();
}
}
}

Getting rid of the checked exception

See how the BusinessLogic class now has a “throws SQLException” declaration on the doBusinessLogic method? It does nothing with this SQLException, it just gets thrown to the controller. Yet it still makes the code depend on the java.sql package.

Your business logic ultimately shouldn’t care if you use a SQL database, NoSQL database or another REST API to fetch the data from. So it’s definitely not nice that this checked SQLException causes the Business logic to know about the java.sql package.

We can solve this by using the @SneakyThrows annotation from the Lombok library:

import lombok.SneakyThrows;.... controller codestatic class BusinessLogic {
private final DatabaseService databaseService;
public BusinessLogic(DatabaseService databaseService) {
this.databaseService = databaseService;
}

public void doBusinessLogic() {
databaseService.executeQuery(new Random().nextBoolean());
}
}

static class DatabaseService {
@SneakyThrows
public void runQuery(boolean extraParam) {
Connection con = DriverManager.getConnection(
"jdbc:hsqldb:mem:testdb;DB_CLOSE_DELAY=-1"
); // Some simple in memory db
String sql = "select * from records where extraParam = ?";
PreparedStatement ps = con.prepareStatement(sql);
ps.setString(1, Boolean.toString(extraParam));
ps.executeQuery();
con.close();
}
}

When the executeQuery method is annotated with @SneakyThrows, it is no longer forced by the compiler to put throws SQLException behind the consuming methods.

Luckily, @SneakyThrows is only needed in very few places. If you use the Spring framework (like 90% of the Java backends use), you will most likely use the JdbcTemplate instead of the original JDBC classes. The JdbcTemplate already catches all SqlExceptions and rethrows them as unchecked exceptions.

Catching exceptions in a real Spring Controller

The above examples are not real “controllers”. Here is an example of catching Exceptions in a RestController using Spring:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GetSomethingController {
private final BusinessLogic businessLogic;

@Autowired
public GetSomethingController(BusinessLogic businessLogic) {
this.businessLogic = businessLogic;
}

@GetMapping("/")
public ResponseEntity<String> getSomething() {
try {
// This might throw an exception...
businessLogic.doBusinessLogic();
return ResponseEntity.ok("Hello");
} catch (Exception e) {
// You should log the exception here!
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Error");
}

}
}

Now we are catching any exception at the system boundary right? Great! Well this is usually not how Exception handling in Spring works.

Using the @ExceptionHandler annotation

See this this Spring boot example code from Baeldung:

@RestController
@RequestMapping("books-rest")
public class SimpleBookRestController {

@GetMapping("/{id}", produces = "application/json")
public Book getBook(@PathVariable int id) {
return findBookById(id);
}
}

They have no ResponseEntity objects in the return types, and they don’t have have try catch statements in the controller either. We could add the try catch, but with the return type Book we can’t return a ResponseEntity object with an INTERNAL_ERROR (500) HTTP status.

We can modify our Spring example to leave out the ResponseEntity class, and throw a ResponseStatusException (available since Spring 5) to make Spring respond with a 500 response:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;

@RestController
public class GetSomethingController {
private final BusinessLogic businessLogic;

@Autowired
public GetSomethingController(BusinessLogic businessLogic) {
this.businessLogic = businessLogic;
}

@GetMapping("/")
public String getSomething() {
try {
// This might throw an exception...
businessLogic.doBusinessLogic();
return "Hello";
} catch (Exception e) {
// You should log the exception here!
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Error");
}
}
}

However this still clutters your controller code. Especially if your controller has 10 methods that all have this try catch pattern. Instead we can define a method just for handling Exceptions and annotate it with the @ExceptionHandler annotation.

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class GetSomethingController {
private final BusinessLogic businessLogic;

@Autowired
public GetSomethingController(BusinessLogic businessLogic) {
this.businessLogic = businessLogic;
}

@GetMapping("/")
public String getSomething() {
businessLogic.doBusinessLogic();
return "Hello";
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleUnexpectedErrors(Exception e) {
// You should log the exception here!
return "Error";
}

}

Such a method handles exceptions that occur in all GET/POST/PUT (or any other) mappings in the controller. You could even define a global one that applies to all controllers.

Adding logging

My previous code snippets already gave it away, we should put logging in the Exception handler. I’m using the @Slf4j annotation from Lombok for logging:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestController
public class GetSomethingController {
private final BusinessLogic businessLogic;

@Autowired
public GetSomethingController(BusinessLogic businessLogic) {
this.businessLogic = businessLogic;
}

@GetMapping("/")
public String getSomething() {
businessLogic.doBusinessLogic();
return "Hello";
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleUnexpectedErrors(HttpServletRequest req, Exception e) {
log.error("Unexpected error occurred on request: " + req.getServletPath(), e);
return "Error";
}
}

If you now run your project with mvn spring-boot:run you should see exceptions in your logs.

Why logging on the boundaries sometimes isn’t enough…

I know I just talked you into catching and logging the exceptions on the system boundaries. But sometimes to troubleshoot you need more. You might want to log the values of certain query parameters when a database query fails. If we try to log at the system boundary (the controller), we don’t have these parameters anymore! So we must do this in the database service itself.

Here is my DatabaseService.java that uses the JdbcTemplate of the Spring framework. This query will fail in my project simply because there is no “records” table.

import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

@Service
@Slf4j
public class DatabaseService {
private JdbcTemplate jdbcTemplate;

public DatabaseService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public void runQuery(boolean param) {
String sql = "select * from records where param = ?";
List<String> ids = jdbcTemplate.query(sql,
new RowMapper<String>() {
@Override
@SneakyThrows
public String mapRow(ResultSet rs, int rowNum) {
return rs.getString("id");
}
}, param);

log.info("We retrieved " + ids.size() + " records.");
}
}

When running this you’ll see that actually a SQLSyntaxErrorException (extends SQLException and thus is a checked exception) is happening. The spring JDBC wraps it in a BadSqlGrammarException, which is a runtime exception so we don’t have to put @SneakyThrows above the runQuery method.

Let’s alter the code to log the error and the parameter:

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;

import java.sql.ResultSet;
import java.util.List;

@Service
@Slf4j
public class DatabaseService {
private JdbcTemplate jdbcTemplate;

public DatabaseService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public void runQuery(boolean param) {
String sql = "select * from records where param = ?";
try {
List<String> ids = jdbcTemplate.query(sql,
new RowMapper<String>() {
@Override
@SneakyThrows
public String mapRow(ResultSet rs,
int rowNum) {
return rs.getString("id");
}
}, param);
log.info("We retrieved " + ids.size() + " records.");
} catch (DataAccessException e) {
log.error("Failed running query: " + sql);
log.error("Parameters: param=" + param);
log.error("Exception: ", e);
throw e;
}

}
}

But wait, if you test with this code, the error is logged twice! Once in the DatabaseService and once in the Controller. That is confusing. We can fix that by creating an AlreadyLoggedException class to wrap around the exception.

public class AlreadyLoggedException extends RuntimeException {
public AlreadyLoggedException(Exception e) {
super(e);
}
}

Make sure to wrap the DataAccessException into this AlreadyLoggedException when rethrowing:

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;

import java.sql.ResultSet;
import java.util.List;

@Service
@Slf4j
public class DatabaseService {
private JdbcTemplate jdbcTemplate;

public DatabaseService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public void runQuery(boolean param) {
String sql = "select * from records where param = ?";
try {
List<String> ids = jdbcTemplate.query(sql,
new RowMapper<String>() {
@Override
@SneakyThrows
public String mapRow(ResultSet rs,
int rowNum) {
return rs.getString("id");
}
}, param);
log.info("We retrieved " + ids.size() + " records.");
} catch (DataAccessException e) {
log.error("Failed running query: " + sql);
log.error("Parameters: param=" + param);
log.error("Exception: ", e);
throw new AlreadyLoggedException(e);
}
}
}

And finally add an extra @ExceptionHandler method in the controller to handle all AlreadyLoggedExceptions:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestController
public class GetSomethingController {
private final BusinessLogic businessLogic;

@Autowired
public GetSomethingController(BusinessLogic businessLogic) {
this.businessLogic = businessLogic;
}

@GetMapping("/")
public String getSomething() {
businessLogic.doBusinessLogic();
return "Hello";
}

@ExceptionHandler(AlreadyLoggedException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleUnexpectedErrorsThatAreAlreadyLogged() {
// Do not log
return "Error";
}


@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleUnexpectedErrors(HttpServletRequest req, Exception e) {
log.error("Unexpected error occurred on request: " + req.getServletPath(), e);
return "Error";
}
}

Done, no more duplicate error logging.

Tracing back client errors to the correct Exception and Stack trace in the logs

In all companies I’ve worked for, it was normal to respond with a HTTP 500 status in case of exceptions. But how do you troubleshoot this when such things happen on production? Imagine the following situation:

Let’s say you’re the backend guy. The support desk tells you users are complaining that the button on the frontend gives generic errors.

What typically happens is that you’re going to ask one user the moment he got the error. If this user bothers to reply, he might give an inaccurate timestamp. If your log is full different of errors, it might be hard to find out what’s happening.

Generating a unique ID for every error

At the last few companies I’ve worked I have advocated to generate an ID for every exception and add it to the logs (along with the stacktrace). In the HTTP response, return this ID. That way your frontend can display it to the end user. When the end user complains to your customer service, the customer service can ask for this ID and pass it to the developer. With that ID, the developer can do a simple search in the logs to find the exact log message that caused the user’s problem.

Here’s that in a diagram:

Implementing logging id’s and putting them in the HTTP response

We can generate a unique identifier (UUID) in Java to make sure we generate something that is hopefully unique within our logs:

Let’s add it to our exception handler method.

import java.util.UUID;// other code@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleUnexpectedErrors(HttpServletRequest req, Exception e) {
final String uuid = UUID.randomUUID().toString();
log.error(uuid + " Unexpected error occurred on request: " + req.getServletPath(), e);
return "Error: " + uuid;
}

To make this work for the exceptions that are caught on other places in the code such as the DatabaseService, we need to alter our AlreadyLoggedException so it can bring the UUID to the controller.

import lombok.Getter;

public class AlreadyLoggedException extends RuntimeException {
@Getter
private final String uuid;


public AlreadyLoggedException(Exception e, final String uuid) {
super(e);
this.uuid = uuid;
}
}

Now we can update our DatabaseService to pass the UUID to the AlreadyLoggedException.

import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Service;

import java.sql.ResultSet;
import java.util.List;
import java.util.UUID;

@Service
@Slf4j
public class DatabaseService {
private JdbcTemplate jdbcTemplate;

public DatabaseService(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public void runQuery(boolean param) {
String sql = "select * from records where param = ?";
try {
List<String> ids = jdbcTemplate.query(sql,
new RowMapper<String>() {
@Override
@SneakyThrows
public String mapRow(ResultSet rs, int rowNum) {
return rs.getString("id");
}
}, param);
log.info("We retrieved " + ids.size() + " records.");
} catch (DataAccessException e) {
final String uuid = UUID.randomUUID().toString();
log.error(uuid + "Failed running query: " + sql);
log.error(uuid + "Parameters: param=" + param);
log.error(uuid + "Exception: ", e);
throw new AlreadyLoggedException(e, uuid);
}
}
}

If you have to do this on many places, you might want to create a utility method to log the error and generate a UUID in there.

Finally, we can update the AlreadyLoggedException handler in the controller and make it pass the UUID to the HTTP response.

@ExceptionHandler(AlreadyLoggedException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleUnexpectedErrorsThatAreAlreadyLogged(
AlreadyLoggedException e) {
// Do not log
return "Error: " + e.getUuid();
}

Here is what is looks like when you run the code:

In my example, I’ve not defined a whole API spec to keep it simple. If you have an API based on an OpenAPI spec, you want to define the JSON of the error responses and include this error UUID.

Using Spring cloud sleuth

When I wanted to implement passing a UUID back to the frontend at work I noticed that our project already used Sleuth for tracing. Sleuth provides autoconfiguration for distributed tracing. Distributed tracing is way beyond the scope of this article, but it does automatically add an id to every log message in your code. This id is unique for every incoming request.

Somehow it is printed twice, I’m not sure why.

To get this behavior, all you need to do is add this dependency to your spring boot application:

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>

You might also need to add this to your dependencyManagement if you haven’t already:

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

You can easily auto wire the Sleuth “tracer” in any Spring component to get the uuid and put it in your HTTP response:

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;

@Slf4j
@RestController
public class GetSomethingController {

private final BusinessLogic businessLogic;
private final Tracer tracer;

@Autowired
public GetSomethingController(Tracer tracer, BusinessLogic businessLogic) {
this.tracer = tracer;
this.businessLogic = businessLogic;
}

@GetMapping("/")
public String getSomething(String name) {
businessLogic.doBusinessLogic();
return "Hello: " + name;
}

@ExceptionHandler(AlreadyLoggedException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleUnexpectedErrorsThatAreAlreadyLogged() {
// Do not log
return "Error: " + tracer.currentSpan().context().traceId();
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public String handleUnexpectedErrors(HttpServletRequest req, Exception e) {
log.error("Unexpected error occurred on request: " + req.getServletPath(), e);
return "Error: " + tracer.currentSpan().context().traceId();
}

// Other handlers
}

Putting the trace id in a header

You could also add a filter to add a header with the trace ID in any response. That way you can track any request, not just the ones that give an error:

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.sleuth.Tracer;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;

import java.io.IOException;

@Component
public class AddTraceIdToResponseFilter extends GenericFilterBean {
private Tracer tracer;

@Autowired
public AddTraceIdToResponseFilter(Tracer tracer) {
this.tracer = tracer;
}

@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain
) throws IOException, ServletException {
if (response instanceof HttpServletResponse) {
String traceId = tracer.currentSpan().context().traceId();
HttpServletResponse res = (HttpServletResponse) response;
res.addHeader("LEONS-TRACE-ID", traceId);
}
chain.doFilter(request, response);
}
}

When you run the application again with mvn spring-boot:run you should see the response header being added.

Validation errors

In the GET request of my examples there is no validation. If you add POST or PUT requests you might need to add it. When we add a POST call to our RestController, we can put the @Valid annotation before the SomePost parameter to make Spring validate it:

import javax.validation.Valid;@Slf4j
@RestController
public class GetSomethingController {
// Other code @PostMapping("/post")
public String postSomething(@Valid @RequestBody SomePost somePost)
{
log.info(somePost.getEmail());
return "Posted";
}
// Exception handlers}

We need to define the SomePost class with some validation annotations to tell Spring how to validate it:

import lombok.AllArgsConstructor;
import lombok.Data;

import javax.validation.constraints.*;

@AllArgsConstructor
@Data
public class SomePost {
@NotNull(message = "'id' field not found")
@Min(value = 0L, message = "The value must be a positive integer in the 'id' field.")

private int id;

@NotNull(message = "'message' field not found")
@Size(min = 5, max = 200, message = "You must enter a message of min 5 characters and max 200 in the 'message' field.")

private String message;

@NotNull(message = "'email' field not found")
@Email(message = "Enter a valid e-mail address in the 'email' field.")

private String email;
}

If we now run our application with mvn spring-boot:run and execute a curl command with an invalid e-mail in the request body:

curl -X POST http://localhost:8080/post -H 'Content-Type: application/json' -d '{"id":"9001","message":"My message", "email":"invalidemail"}'

When Spring attempts to validate this request it will fail the validation and throw a MethodArgumentNotValidException. Our exception handler logs the stacktrace below and throws a 500:

org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String net.leejjon.demo.GetSomethingController.postSomething(net.leejjon.demo.SomePost): [Field error in object 'somePost' on field 'email': rejected value [invalidemail]; codes [Email.somePost.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [somePost.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@16ee46a2,.*]; default message [must be a well-formed email address]] 
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:141) ~[spring-webmvc-5.3.14.jar:5.3.14]

Apart from this stacktrace not being useful as our application is not doing anything wrong, the service should respond with 400 (BAD_REQUEST).

So let’s write a separate exception handler for validation errors:

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public String handleValidationErrors(HttpServletRequest req, MethodArgumentNotValidException e) {
StringBuilder validationErrorMessage = new StringBuilder("Validation error: \n");
for (ObjectError vallidationError : e.getAllErrors()) {
validationErrorMessage.append(vallidationError.getObjectName());
validationErrorMessage.append(" ");
validationErrorMessage.append(vallidationError.getDefaultMessage());
validationErrorMessage.append("\n");
}

return validationErrorMessage.toString();
}

Now the curl command will get a 400 response with the following body:

Validation error: Enter a valid e-mail address.

Too long didn’t read

Things to remember:

  • Catch all exceptions at the system boundaries. In a REST service this means the controller.
  • Log your exceptions with a randomly generated UUID.
  • Return the generated UUID to the consumer of your service. If they complain that something doesn’t work, ask for this UUID so you can find to the related exception in your logs.
  • Don’t log validation failures. Provide good API documentation so the consumer of your API can figure out what to do if they get 400’s back from your service.

The full code can be found on GitHub. There might be a 100 ways to improve this. If you think I might be missing something essential, let me know!

--

--

Leejjon

Java/TypeScript Developer. Interested in web/mobile/backend/database/cloud. Freelancing, only interested in job offers from employers directly. No middle men.