From 7c613daeda5ed1b3237ecf003bad5282a3ca308d Mon Sep 17 00:00:00 2001 From: Oleg Zhurakousky Date: Thu, 9 Mar 2023 16:00:06 +0100 Subject: [PATCH] Add initial support for error handling --- .../serverless/web/ProxyErrorController.java | 81 +++++++++++++++++++ .../web/ProxyHttpServletResponse.java | 2 +- .../function/serverless/web/ProxyMvc.java | 45 ++++++++++- .../serverless/web/RequestResponseTests.java | 20 ++++- .../function/test/app/PetsController.java | 14 +++- 5 files changed, 157 insertions(+), 5 deletions(-) create mode 100644 spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyErrorController.java diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyErrorController.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyErrorController.java new file mode 100644 index 000000000..9a4df17e9 --- /dev/null +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyErrorController.java @@ -0,0 +1,81 @@ +/* + * Copyright 2023-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.function.serverless.web; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.RequestDispatcher; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.json.MappingJackson2JsonView; + +/** + * + * @author Oleg Zhurakousky + * + */ +@Controller +@RequestMapping("/error") +public class ProxyErrorController { + + private final SimpleDateFormat df = new SimpleDateFormat("E, dd MMM yyyy HH:mm:ss z"); + + private final MappingJackson2JsonView view = new MappingJackson2JsonView(); + + @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) + public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { + HttpStatus status = getStatus(request); + Map model = new HashMap<>(); + model.put("status", response.getStatus()); + model.put("error", request.getAttribute(RequestDispatcher.ERROR_MESSAGE)); + model.put("path", request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI)); + model.put("timestamp", df.format(new Date())); + response.setStatus(status.value()); + ModelAndView modelAndView = resolveErrorView(request, response, status, model); + return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); + } + + protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status, + Map model) { + ModelAndView modelAndView = new ModelAndView("Whitelabel Error Page", model); + modelAndView.setStatus(status); + modelAndView.setView(this.view); + return modelAndView; + } + + protected HttpStatus getStatus(HttpServletRequest request) { + Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); + if (statusCode == null) { + return HttpStatus.INTERNAL_SERVER_ERROR; + } + try { + return HttpStatus.valueOf(statusCode); + } + catch (Exception ex) { + return HttpStatus.INTERNAL_SERVER_ERROR; + } + } +} diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyHttpServletResponse.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyHttpServletResponse.java index e57251e5a..93cd072ea 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyHttpServletResponse.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyHttpServletResponse.java @@ -162,7 +162,7 @@ public class ProxyHttpServletResponse implements HttpServletResponse { @Override public boolean isCommitted() { - return true; + return false; } @Override diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyMvc.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyMvc.java index 51fd12b55..8a30e8557 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyMvc.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/main/java/org/springframework/cloud/function/serverless/web/ProxyMvc.java @@ -27,6 +27,7 @@ import java.util.List; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; +import javax.servlet.RequestDispatcher; import javax.servlet.Servlet; import javax.servlet.ServletConfig; import javax.servlet.ServletContext; @@ -36,10 +37,15 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + import org.springframework.context.annotation.AnnotatedBeanDefinitionReader; +import org.springframework.http.HttpStatus; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; import org.springframework.web.context.ConfigurableWebApplicationContext; import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.servlet.DispatcherServlet; @@ -55,6 +61,8 @@ import org.springframework.web.servlet.DispatcherServlet; */ public class ProxyMvc { + private static Log LOG = LogFactory.getLog(ProxyMvc.class); + static final String MVC_RESULT_ATTRIBUTE = ProxyMvc.class.getName().concat(".MVC_RESULT_ATTRIBUTE"); private final DispatcherServlet servlet; @@ -71,6 +79,8 @@ public class ProxyMvc { AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(applpicationContext); reader.register(componentClasses); + reader.register(ProxyErrorController.class); + try { DispatcherServlet servlet = new DispatcherServlet(applpicationContext); servlet.init(new ProxyServletConfig(servletContext)); @@ -199,7 +209,40 @@ public class ProxyMvc { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { - this.delegateServlet.service(request, response); + try { + this.delegateServlet.service(request, response); + if (((HttpServletResponse) response).getStatus() != HttpStatus.OK.value()) { + ((ProxyHttpServletRequest) request).setAttribute(RequestDispatcher.ERROR_STATUS_CODE, ((HttpServletResponse) response).getStatus()); + this.setErrorMessageAttribute((ProxyHttpServletRequest) request, (ProxyHttpServletResponse) response, null); + ((ProxyHttpServletRequest) request).setAttribute(RequestDispatcher.ERROR_REQUEST_URI, ((ProxyHttpServletRequest) request).getRequestURI()); + + ((ProxyHttpServletRequest) request).setRequestURI("/error"); + this.delegateServlet.service(request, response); + } + } + catch (Exception e) { + ((ProxyHttpServletRequest) request).setAttribute(RequestDispatcher.ERROR_STATUS_CODE, HttpStatus.INTERNAL_SERVER_ERROR); + this.setErrorMessageAttribute((ProxyHttpServletRequest) request, (ProxyHttpServletResponse) response, e); + ((ProxyHttpServletRequest) request).setAttribute(RequestDispatcher.ERROR_EXCEPTION_TYPE, e); + ((ProxyHttpServletRequest) request).setAttribute(RequestDispatcher.ERROR_REQUEST_URI, ((ProxyHttpServletRequest) request).getRequestURI()); + + LOG.error("Failed processing the request to: " + ((ProxyHttpServletRequest) request).getRequestURI(), e); + ((ProxyHttpServletRequest) request).setRequestURI("/error"); + this.delegateServlet.service(request, response); + } + } + + private void setErrorMessageAttribute(ProxyHttpServletRequest request, ProxyHttpServletResponse response, Exception exception) { + if (exception != null && StringUtils.hasText(exception.getMessage())) { + request.setAttribute(RequestDispatcher.ERROR_MESSAGE, exception.getMessage()); + } + else if (StringUtils.hasText(response.getErrorMessage())) { + request.setAttribute(RequestDispatcher.ERROR_MESSAGE, response.getErrorMessage()); + } + else { + request.setAttribute(RequestDispatcher.ERROR_MESSAGE, HttpStatus.valueOf(response.getStatus()).getReasonPhrase()); + + } } @Override diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java index 348d3b264..3af280e3a 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/serverless/web/RequestResponseTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.springframework.cloud.function.test.app.Pet; import org.springframework.cloud.function.test.app.PetStoreSpringAppConfig; +import org.springframework.http.HttpStatus; import static org.assertj.core.api.Assertions.assertThat; @@ -44,7 +45,7 @@ public class RequestResponseTests { @BeforeEach public void before() { - this.mvc = ProxyMvc.INSTANCE(PetStoreSpringAppConfig.class); + this.mvc = ProxyMvc.INSTANCE(PetStoreSpringAppConfig.class, ProxyErrorController.class); } @AfterEach @@ -87,6 +88,23 @@ public class RequestResponseTests { assertThat(pet.getName()).isNotEmpty(); } + @Test + public void errorThrownFromMethod() throws Exception { + HttpServletRequest request = new ProxyHttpServletRequest(null, "GET", "/pets/2"); + ProxyHttpServletResponse response = new ProxyHttpServletResponse(); + mvc.service(request, response); + assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); + assertThat(response.getErrorMessage()).isEqualTo("No such Dog"); + } + + @Test + public void errorUnexpectedWhitelabel() throws Exception { + HttpServletRequest request = new ProxyHttpServletRequest(null, "GET", "/pets/2/3/4"); + ProxyHttpServletResponse response = new ProxyHttpServletResponse(); + mvc.service(request, response); + assertThat(response.getStatus()).isEqualTo(HttpStatus.NOT_FOUND.value()); + } + @Test public void validatePostWithBody() throws Exception { ProxyHttpServletRequest request = new ProxyHttpServletRequest(null, "POST", "/pets/"); diff --git a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetsController.java b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetsController.java index 0d3f9b9d1..a470d8135 100644 --- a/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetsController.java +++ b/spring-cloud-function-adapters/spring-cloud-function-serverless-web/src/test/java/org/springframework/cloud/function/test/app/PetsController.java @@ -20,10 +20,13 @@ import java.security.Principal; import java.util.Optional; import java.util.UUID; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @@ -63,8 +66,10 @@ public class PetsController { } @RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET) - public Pet listPets() { - System.out.println("=====> Getting pet by id"); + public Pet listPets(@PathVariable String petId) { + if (petId.equals("2")) { + throw new DogNotFoundException(); + } Pet newPet = new Pet(); newPet.setId(UUID.randomUUID().toString()); newPet.setBreed(PetData.getRandomBreed()); @@ -72,4 +77,9 @@ public class PetsController { newPet.setName(PetData.getRandomName()); return newPet; } + + @ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No such Dog") // 404 + public class DogNotFoundException extends RuntimeException { + // ... + } }