Skip to content

Commit

Permalink
Allow custom error pages to be used when an application behind the ga…
Browse files Browse the repository at this point in the history
…teway returns an error.

`GatewayFilterFactory` providing a `GatewayFilter` that throws a
`ResponseStatusException` with the proxied response status code if the
target responded with a `400` or `500` status code.

Usage: to enable it globally, add this to application.yaml :

```
 spring:
  cloud:
    gateway:
      default-filters:
        - ApplicationError
```

To enable it only on some routes, add this to concerned routes
in `routes.yaml`:

```
        filters:
       - name: ApplicationError
```
  • Loading branch information
emmdurin authored and groldan committed May 27, 2024
1 parent 1995564 commit e7d266a
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 1 deletion.
22 changes: 22 additions & 0 deletions docs/custom-error-pages.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,25 @@ spring:
3. In the datadir create a folder from the root directory: "gateway/templates/error"
4. Place your error page files named as per the status code. For example for 404: 404.html
5. Restart georchestra gateway.

== Using custom error pages for applications errors

Custom error pages can also be used when an application behind the gateways returns an error.

To enable it globally, add this to application.yaml :
[application.yaml]
----
spring:
cloud:
gateway:
default-filters:
- ApplicationError
----

To enable it only on some routes, add this to concerned routes in routes.yaml :
[routes.yaml]
----
filters:
- name: ApplicationError
----

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@

import org.georchestra.gateway.filter.global.ResolveTargetGlobalFilter;
import org.georchestra.gateway.filter.headers.HeaderFiltersConfiguration;
import org.georchestra.gateway.filter.global.ApplicationErrorGatewayFilterFactory;
import org.georchestra.gateway.model.GatewayConfigProperties;
import org.georchestra.gateway.model.GeorchestraTargetConfig;
import org.geoserver.cloud.gateway.filter.RouteProfileGatewayFilterFactory;
Expand All @@ -44,7 +45,7 @@ public class FiltersAutoConfiguration {
* matched Route's GeorchestraTargetConfig for each HTTP request-response
* interaction before other filters are applied.
*/
public @Bean ResolveTargetGlobalFilter resolveTargetWebFilter(GatewayConfigProperties config) {
@Bean ResolveTargetGlobalFilter resolveTargetWebFilter(GatewayConfigProperties config) {
return new ResolveTargetGlobalFilter(config);
}

Expand All @@ -64,4 +65,8 @@ public class FiltersAutoConfiguration {
public @Bean StripBasePathGatewayFilterFactory stripBasePathGatewayFilterFactory() {
return new StripBasePathGatewayFilterFactory();
}

@Bean ApplicationErrorGatewayFilterFactory applicationErrorGatewayFilterFactory() {
return new ApplicationErrorGatewayFilterFactory();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright (C) 2024 by the geOrchestra PSC
*
* This file is part of geOrchestra.
*
* geOrchestra is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* geOrchestra is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* geOrchestra. If not, see <http://www.gnu.org/licenses/>.
*/
package org.georchestra.gateway.filter.global;

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.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

/**
* {@link GatewayFilterFactory} providing a {@link GatewayFilter} that throws a
* {@link ResponseStatusException} with the proxied response status code if the
* target responded with a {@code 400...} or {@code 500...} status code.
*
*/
public class ApplicationErrorGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {

public ApplicationErrorGatewayFilterFactory() {
super(Object.class);
}

@Override
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) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
HttpStatus statusCode = exchange.getResponse().getStatusCode();
if (statusCode.is4xxClientError() || statusCode.is5xxServerError()) {
throw new ResponseStatusException(statusCode);
}
}));
}

@Override
public int getOrder() {
return ResolveTargetGlobalFilter.ORDER + 1;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright (C) 2024 by the geOrchestra PSC
*
* This file is part of geOrchestra.
*
* geOrchestra is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option)
* any later version.
*
* geOrchestra is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* geOrchestra. If not, see <http://www.gnu.org/licenses/>.
*/
package org.georchestra.gateway.filter.global;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;

import java.net.URI;
import java.util.List;

import org.georchestra.gateway.model.HeaderMappings;
import org.georchestra.gateway.model.RoleBasedAccessRule;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.http.HttpStatus;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ResponseStatusException;

import reactor.core.publisher.Mono;

class ApplicationErrorGatewayFilterFactoryTest {


private GatewayFilterChain chain;
private GatewayFilter filter;
private MockServerWebExchange exchange;

final URI matchedURI = URI.create("http://fake.backend.com:8080");
private Route matchedRoute;

HeaderMappings defaultHeaders;
List<RoleBasedAccessRule> defaultRules;

@BeforeEach
void setUp() throws Exception {
var factory = new ApplicationErrorGatewayFilterFactory();
filter = factory.apply(factory.newConfig());

matchedRoute = mock(Route.class);
when(matchedRoute.getUri()).thenReturn(matchedURI);

chain = mock(GatewayFilterChain.class);
when(chain.filter(any())).thenReturn(Mono.empty());
MockServerHttpRequest request = MockServerHttpRequest.get("/test").build();
exchange = MockServerWebExchange.from(request);
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, matchedRoute);
}

@Test
void testNotAnErrorResponse() {
exchange.getResponse().setStatusCode(HttpStatus.OK);
Mono<Void> result = filter.filter(exchange, chain);
result.block();
assertThat(exchange.getResponse().getRawStatusCode()).isEqualTo(200);
}

@Test
void test4xx() {
testApplicationError(HttpStatus.BAD_REQUEST);
testApplicationError(HttpStatus.UNAUTHORIZED);
testApplicationError(HttpStatus.FORBIDDEN);
testApplicationError(HttpStatus.NOT_FOUND);
}


@Test
void test5xx() {
testApplicationError(HttpStatus.INTERNAL_SERVER_ERROR);
testApplicationError(HttpStatus.SERVICE_UNAVAILABLE);
testApplicationError(HttpStatus.BAD_GATEWAY);
}

private void testApplicationError(HttpStatus status) {
exchange.getResponse().setStatusCode(status);
Mono<Void> result = filter.filter(exchange, chain);
ResponseStatusException ex = assertThrows(ResponseStatusException.class, ()-> result.block());
assertThat(ex.getStatus()).isEqualTo(status);
}
}

0 comments on commit e7d266a

Please sign in to comment.