Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Global Error Handling for Spring WebFlux Functional REST APIs

Tech 1

Centralized exception handling in reactive REST services avoids duplicating try/catch logic across handlers, produces consistent error payloads, and centralizes error code management.

  • Single place to map exceptions to HTTP status codes and payloads
  • Consistent JSON shape for all failures
  • Cleaner handler functions with business logic only

Project setup

  • Runtime: JDK 8+, Maven 3+
  • Stack: Spring Boot 2.x, spring-boot-starter-webflux
  • No application.properties required for this example

Functional routing

Map a GET endpoint to a handler using RouterFunction.

// ApiRoutes.java
package com.example.webflux.error;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RequestPredicate;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.reactive.function.server.RequestPredicates;

@Configuration
public class ApiRoutes {

    @Bean
    public RouterFunction<ServerResponse> greetingRoutes(GreetingHandler handler) {
        RequestPredicate predicate = RequestPredicates
                .GET("/greet")
                .and(RequestPredicates.accept(MediaType.TEXT_PLAIN));

        return RouterFunctions.route(predicate, handler::greet);
    }
}

RouterFunction routes any GET /greet request with text/plain to the greet handler method.

Handler

Validate input and, if invalid, throw a domain-specific exception that the global handler will translate into an error response.

// GreetingHandler.java
package com.example.webflux.error;

import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;

@Component
public class GreetingHandler {

    public Mono<ServerResponse> greet(ServerRequest req) {
        return Mono.defer(() -> Mono.just(
                req.queryParam("name")
                   .filter(s -> !s.trim().isEmpty())
                   .orElseThrow(() -> new ApiException(HttpStatus.BAD_REQUEST,
                           "query parameter 'name' is required"))
        )).flatMap(name -> ServerResponse.ok()
                .contentType(MediaType.TEXT_PLAIN)
                .bodyValue("Hi, " + name));
    }
}
  • Mono is used because each request returns a single response.
  • Throwing ApiException lets downstream global handlers produce a unified JSON error.

Domain excepttion

Use ResponseStatusException to carry HTTP status and reason across the reactive pipeline.

// ApiException.java
package com.example.webflux.error;

import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;

public class ApiException extends ResponseStatusException {
    public ApiException(HttpStatus status, String reason) {
        super(status, reason);
    }

    public ApiException(HttpStatus status, String reason, Throwable cause) {
        super(status, reason, cause);
    }
}

Error attributes

Customize error attributes to shape the JSON payload returned to clients.

// ApiErrorAttributes.java
package com.example.webflux.error;

import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;

import java.util.LinkedHashMap;
import java.util.Map;

@Component
public class ApiErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
        Map<String, Object> base = super.getErrorAttributes(request, includeStackTrace);
        Throwable error = getError(request);

        Map<String, Object> attrs = new LinkedHashMap<>();
        if (error instanceof ApiException) {
            ApiException ex = (ApiException) error;
            attrs.put("exception", ex.getClass().getSimpleName());
            attrs.put("message", ex.getReason());
            attrs.put("status", ex.getStatus().value());
            attrs.put("error", ex.getStatus().getReasonPhrase());
        } else {
            attrs.put("exception", error != null ? error.getClass().getSimpleName() : "UnknownException");
            attrs.put("message", "Unexpected error. See logs for details.");
            attrs.put("status", 500);
            attrs.put("error", "Internal Server Error");
        }
        // Optional: include path and timestamp from the base map
        attrs.put("path", base.get("path"));
        attrs.put("timestamp", base.get("timestamp"));
        return attrs;
    }
}

Global error WebExceptionHandler

Route all exceptions to a single renderer that converts ApiErrorAttributes into a resopnse.

// ApiErrorWebExceptionHandler.java
package com.example.webflux.error;

import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.*;
import org.springframework.web.reactive.result.view.ViewResolver;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.http.HttpStatus;
import org.springframework.http.codec.ServerCodecConfigurer;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Map;

@Component
@Order(-2)
public class ApiErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {

    public ApiErrorWebExceptionHandler(ApiErrorAttributes errorAttributes,
                                       ApplicationContext applicationContext,
                                       ServerCodecConfigurer codecConfigurer) {
        super(errorAttributes, new ResourceProperties(), applicationContext);
        super.setMessageReaders(codecConfigurer.getReaders());
        super.setMessageWriters(codecConfigurer.getWriters());
    }

    @Override
    protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
        return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
    }

    private Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
        Map<String, Object> attributes = getErrorAttributes(request, false);
        int status = (int) attributes.getOrDefault("status", 500);
        return ServerResponse.status(status)
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(attributes));
    }
}
  • @Order(-2) ensures this handler runs before the default one.
  • All exceptions are routed to renderErrorResponse, which builds a JSON body from custom attributes and uses the status code provided by those attributes.

Bootstrapping

// Application.java
package com.example.webflux.error;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Manual verification

  • Missing parameter (error case):

    • Request: GET http://localhost:8080/greet
    • Response (HTTP 400): {"exception":"ApiException","message":"query parameter 'name' is required","status":400,"error":"Bad Request","path":"/greet","timestamp":"..."}
  • Valid parameter (success case):

Design notes

  • Derive domain exceptions from ResponseStatusException to carry status and reason.
  • Keep error payloads stable and versionable; include fields like status, error, message, path, and a machine-readable code if needed.
  • Handlers remain focused on bussiness logic; conversion to error responses is centralized via ErrorAttributes and AbstractErrorWebExceptionHandler.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.