R.Litto
R.Litto

Reputation: 393

Spring Cloud Gateway - unhappy path handling in filters (Exception Handling vs Response manipulation) with Response body

What is the best (aka most efficient way) of implementing the management of unhappy paths with SCG Filters and Response with a body? Exception handling or response rewriting?

Scenario

We are implementing an API Gateway with some complex GatewayFilters (between different type of Throttling and some other behaviours) that have in common an unhappy path where they terminate the exchange chain processing and should return immediately a 4xx response with a specific common body format (json/xml based on Accept header, we have legacy compatibility requirements).

Initial solution

My initial solution was to use the filter and then try to write the response with one of those options:

response.setStatusCode(..);
//set headers
return response.writeAndFlushWith(...);

or

response.setStatusCode(..);
//set headers
return response.writeWith(...);

or even

response.setStatusCode(..);
exchange.mutate().response(...);
return response.setComplete();

in each specific filter. But the processing of the body response seems quite a verbose process, plus the formatting of the error provided by the filter should not be a concern of the specific filter. We want a separation of concerns, so the filter should be defining headers, status and error message but someone else should transform that in the proper response format.

So after a long (and bloody :) ) discussion we come up with two possible options. Let's assume we have 4 Filters and our happy path would be (with some approximation non considering overlapping from global filters order):

(initial global filters) -> 1 -> 2 -> 3 -> 4-> 5 -> (other global filters) -> Routing -> (other global filters) -> 5 -> 4-> 3 -> 2 -> 1 -> 0 -> (initial global filters) -> Response

Option 1 - Error Handling

Implement a class java MyErrorHandler extends DefaultErrorWebExceptionHandler and java MyErrorAttributes extends DefaultErrorAttributes and that does the magic.

This means in the specific filter throwing an exception that extends ResponseStatusException or is annotated with @ResponseStatus and have it bubble out to the ExceptionHandler This has the advantage of interrupting all the exchange processing in the filters, too, if I am not wrong. Eg: given 3 filters and filter 3 throwing an exception we have ... 1 -> 2 -> 3 -> ExH -> (other global filters) -> Response

Option 2 - Response Writing

Add a filter (filter 0) in the topmost position, maybe one extending ModifyResponseGatewayFilter that manages errors writing in the response by looking at a specific exchange attribute (eg:gateway.error) for a bean and writes the response, and have the filter in the chain set that specific bean and generate the response with that.

This would have the following flow : ... -> 0 -> 1 -> 2 -> 3 -> 2 -> 1 -> 0 -> (other global filters) -> Response

Issues

Option

  1. Option 1 seems the short circuit option that really blocks the processing and builds on existing capabilities (ExceptionHandler) but I am told Exceptions are a terrible burden for efficiency since they are forced to build all the stacktrace
  2. Option 2 seems to be more efficient, but then the response bubbles up through all the "post" filters and we must make sure that no other filter tries to update the response by checking the specific exchange attribute (eg:gateway.error)

The RedisRateLimiter offered by SCG uses option2 but does no body rewriting and uses response.setComplete() but this doesn't allow any body rewriting later.

Is this the preferred approach?

Upvotes: 2

Views: 5660

Answers (2)

YAO ALEX DIDIER AKOUA
YAO ALEX DIDIER AKOUA

Reputation: 249

you can handle Error, with one class which implements ErrorWebExceptionHandler

@Component
@Primary
@Slf4j
public class ErrorResponseFilter implements ErrorWebExceptionHandler {

@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
    ...
    return null;
  } 
}

and add this in another class

@Configuration
public class ExceptionConfig {

@Primary
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ErrorWebExceptionHandler errorWebExceptionHandler(ObjectProvider<List<ViewResolver>> viewResolversProvider,
        ServerCodecConfigurer serverCodecConfigurer) {
    ErrorResponseFilter jsonExceptionHandler = new ErrorResponseFilter();
    jsonExceptionHandler.setViewResolvers(viewResolversProvider.getIfAvailable(Collections::emptyList));
    jsonExceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
    jsonExceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
    return jsonExceptionHandler;
 }
}

Upvotes: 0

R.Litto
R.Litto

Reputation: 393

At the end what we went for to reduce unnecessary stacktrace creation was the following:

  1. We returned Mono.error(new GatewayRoutingException(..))
  2. We made GatewayRoutingException extends ResponseStatusException
  3. We overrode fillInStackTrace so not to call the native fillInStackTrace(int)

    public synchronized Throwable fillInStackTrace() { return this; }

  4. We stored the HttpHeaders we wanted to pass together with the HttpStatus

  5. We overrode the ErrorHandler to manage that exception and return a GatewayError bean as a body with our error data structure, our http status and headers from the GatewayRoutingException

Upvotes: 1

Related Questions