spring webflux error handling

Spring WebFlux Error Handling

Overview:

In this article, I would like to show you Spring WebFlux Error Handling using @ControllerAdvice.

Spring WebFlux Error Handling:

Developing Microservices is fun and easy with Spring Boot. But anything could go wrong in when multiple Microservices talk to each other. In case of some unhandled exceptions like 500 – Internal Server Error, Spring Boot might respond as shown here.

{
    "timestamp": "2020-11-02T02:33:08.501+00:00",
    "path": "/movie/action",
    "status": 500,
    "error": "Internal Server Error",
    "message": "",
    "requestId": "a9b4c6d4-4"
}

Usually error messages like this will not be handled properly and would be propagated to all the downstream services which might impact user experience. In some cases, applications might want to use application specific error code to convey appropriate messages to the calling service.

Let’s see how we could achieve that using Spring WebFlux.

Sample Application:

Let’s consider a simple application in which we have couple of APIs to get student information.

  • GET – /student/all
    • This will return all student information. Occasionally this throws some weird exceptions.
  • GET /student/[id]
    • This will return specific student based on the given id.

Project Set up:

Create a Spring application with the following dependencies.

spring webclient with feign

Overall the project structure will be as shown here.

spring webflux error handling

 

DTO:

First I create a simple DTO for student. We are interested only these 3 attributes of student for now.

@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class Student {

    private int id;
    private String firstName;
    private String lastName;

}

Then I create another class to respond in case of error.  errorCode could be some app specific error code and some appropriate error message.

@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class ErrorResponse {

    private int errorCode;
    private String message;

}

I also create another exception class as shown here for the service layer to throw an exception when student is not found for the given id.

public class StudentNotFoundException extends RuntimeException {

    private final int studentId;
    private static final String MESSAGE = "Student not found";

    public StudentNotFoundException(int id) {
        super(MESSAGE);
        this.studentId = id;
    }

    public int getStudentId() {
        return studentId;
    }

}

Student Service:

Then I create a service layer with these 2 methods.

@Service
public class StudentService {

    private static final Map<Integer, Student> DB = Map.of(
            1, new Student(1, "fn1", "ln1"),
            2, new Student(2, "fn2", "ln2"),
            3, new Student(3, "fn3", "ln3"),
            4, new Student(4, "fn4", "ln4"),
            5, new Student(5, "fn5", "ln5")
    );

    public Flux<Student> getAllStudents() throws Exception {
        return Flux.fromIterable(DB.values())
                   .doFirst(this::throwRandomError);
    }

    public Mono<Student> findStudentById(int id) {
        return Mono.just(id)
                   .filter(DB::containsKey)
                   .map(DB::get)
                   .switchIfEmpty(Mono.error(() -> new StudentNotFoundException(id)));
    }

    private void throwRandomError(){
        var random = ThreadLocalRandom.current().nextInt(0, 10);
        if(random > 5)
            throw new RuntimeException("some random error");
    }

}
  • getAllStudents: will throw some exception at random.
  • findStudentById: method will throw exception when the student is not found. It throws StudentNotFoundException.

REST API:

Let’s create a simple StudentController to expose those 2 APIs.

  • /student/all endpoint will fetch all the students
  • /student/{id} endpoint will fetch specific student
@RestController
@RequestMapping("student")
public class StudentController {

    @Autowired
    private StudentService studentService;

    @GetMapping("all")
    public Flux<Student> getAll() throws Exception {
        return this.studentService.getAllStudents();
    }

    @GetMapping("{id}")
    public Mono<Student> getById(@PathVariable("id") int id){
        return this.studentService.findStudentById(id);
    }

}

Now if we run the application and try to access the below URL a few times – will throw RunTimeException.

http://localhost:8080/student/all

The response could be something like this.

{
   "timestamp":"2022-02-01T02:39:47.489+00:00",
   "path":"/student/all",
   "status":500,
   "error":"Internal Server Error",
   "requestId":"cfa1db44-3"
}

Let’s see how we could handle and respond better.

@ControllerAdvice:

Spring provides @ControllerAdvice for handling exceptions in Spring Boot Microservices. The annotated class will act like an Interceptor in case of any exceptions.

  • We can have multiple exception handlers to handle each exception.
  • In our case we throw RunTimeException and StudentNotFoundException – so we have 2 exception handlers. We can also handle all by using a simple Exception.class if we want.
@ControllerAdvice
public class ApplicationErrorHandler {

    @ExceptionHandler(RuntimeException.class)
    public ResponseEntity<ErrorResponse> handleRuntimeException(RuntimeException e){
        var errorResponse = this.buildErrorResponse(100, "Unable to fetch students");
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body(errorResponse);
    }

    @ExceptionHandler(StudentNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleStudentNotFoundException(StudentNotFoundException e){
        var errorResponse = this.buildErrorResponse(101, String.format("Student id %s is not found", e.getStudentId()));
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(errorResponse);
    }

    private ErrorResponse buildErrorResponse(int code, String message){
        return new ErrorResponse(code, message);
    }

}

Spring WebFlux Error Handling – Demo:

If I send below request, I get the appropriate response instead of directly propagating 500 Internal Server Error.

http://localhost:8080/student/35

This error provides more meaningful error message. So the calling service use this error code might take appropriate action.

{
   "errorCode":101,
   "message":"Student id 35 is not found"
}

Similarly, I invoke below endpoint (after few times), then I below response.

http://localhost:8080/student/all
{
   "errorCode":100,
   "message":"Unable to fetch students"
}

Summary:

We were able to demonstrate Spring WebFlux Error Handling using @ControllerAdvice. This is a simple example. There are various other design patterns as well to make the system more resilient which could be more useful for a large application.

Resilient Microservice Design Patterns:

The source code is here.

Happy learning 🙂

Share This:

1 thought on “Spring WebFlux Error Handling

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.