Skip to content

Commit

Permalink
Preserve response headers when redirecting application error to gatew…
Browse files Browse the repository at this point in the history
…ay error pages

Commit 37ff94b make the `ApplicationError` Gateway filter lose the
original response headers when throwing a `ResponseStatusException`
for the Gateway to show up the customized HTML error pages instead of
the orignal (usually whitelabel) errors.

This patch makes it so that the `ApplicationError` filter runs
only when `text/html` is accepted by the request, and the request
method is idempotent (e.g. GET, HEAD, etc.).

Additionally, the original response headers are not lost, since the
exception is thrown at `ServerHttpResponseDecorator.beforeCommit()`, and
respecting the reactive chain.
  • Loading branch information
groldan committed Jul 20, 2024
1 parent a0f0278 commit 0beae05
Show file tree
Hide file tree
Showing 3 changed files with 263 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,19 @@
*/
package org.georchestra.gateway.filter.global;

import java.net.URI;
import java.util.function.Supplier;

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.cloud.gateway.filter.factory.GatewayFilterFactory;
import org.springframework.cloud.gateway.support.HttpStatusHolder;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.lang.Nullable;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;

Expand All @@ -39,7 +39,8 @@

/**
* Filter to allow custom error pages to be used when an application behind the
* gateways returns an error.
* gateways returns an error, only for idempotent HTTP response status codes
* (i.e. GET, HEAD, OPTIONS).
* <p>
* {@link GatewayFilterFactory} providing a {@link GatewayFilter} that throws a
* {@link ResponseStatusException} with the proxied response status code if the
Expand Down Expand Up @@ -80,29 +81,59 @@ public GatewayFilter apply(final Object config) {
return new ServiceErrorGatewayFilter();
}

private static class ServiceErrorGatewayFilter implements GatewayFilter, Ordered {

public @Override Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

ApplicationErrorConveyorHttpResponse response;
response = new ApplicationErrorConveyorHttpResponse(exchange.getResponse());

exchange = exchange.mutate().response(response).build();
return chain.filter(exchange);
private class ServiceErrorGatewayFilter implements GatewayFilter, Ordered {
/**
* @return {@link Ordered#HIGHEST_PRECEDENCE} or
* {@link ApplicationErrorConveyorHttpResponse#beforeCommit(Supplier)}
* won't be called
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}

/**
* If the request method is idempotent and accepts {@literal text/html}, applies
* a filter that when the routed response receives an error status code, will
* throw a {@link ResponseStatusException} with the same status, for the gateway
* to apply the customized error template, also when the status code comes from
* a proxied service response
*/
@Override
public int getOrder() {
return ResolveTargetGlobalFilter.ORDER + 1;
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (canFilter(exchange.getRequest())) {
exchange = decorate(exchange);
}
return chain.filter(exchange);
}
}

ServerWebExchange decorate(ServerWebExchange exchange) {
var response = new ApplicationErrorConveyorHttpResponse(exchange.getResponse());
exchange = exchange.mutate().response(response).build();
return exchange;
}

boolean canFilter(ServerHttpRequest request) {
return methodIsIdempotent(request.getMethod()) && acceptsHtml(request);
}

boolean methodIsIdempotent(HttpMethod method) {
return switch (method) {
case GET, HEAD, OPTIONS, TRACE -> true;
default -> false;
};
}

boolean acceptsHtml(ServerHttpRequest request) {
return request.getHeaders().getAccept().stream().anyMatch(MediaType.TEXT_HTML::isCompatibleWith);
}

/**
* A response decorator that throws a {@link ResponseStatusException} at
* {@link #setStatusCode(HttpStatus)} if the status code is an error code, thus
* letting the gateway render the appropriate custom error page instead of the
* original application response body.
* {@link #beforeCommit} if the status code is an error code, thus letting the
* gateway render the appropriate custom error page instead of the original
* application response body.
*/
private static class ApplicationErrorConveyorHttpResponse extends ServerHttpResponseDecorator {

Expand All @@ -111,12 +142,14 @@ public ApplicationErrorConveyorHttpResponse(ServerHttpResponse delegate) {
}

@Override
public boolean setStatusCode(@Nullable HttpStatus status) {
checkStatusCode(status);
return super.setStatusCode(status);
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
Mono<Void> checkStatus = Mono.fromRunnable(this::checkStatusCode);
Mono<Void> checkedAction = checkStatus.then(Mono.fromRunnable(action::get));
super.beforeCommit(() -> checkedAction);
}

private void checkStatusCode(HttpStatus statusCode) {
private void checkStatusCode() {
HttpStatus statusCode = getStatusCode();
log.debug("native status code: {}", statusCode);
if (statusCode.is4xxClientError() || statusCode.is5xxServerError()) {
log.debug("Conveying {} response status", statusCode);
Expand Down
Loading

0 comments on commit 0beae05

Please sign in to comment.