Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Catch application errors to display standardized error page #122

Merged
merged 1 commit into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
Loading