Add initial support for error handling

This commit is contained in:
Oleg Zhurakousky
2023-03-09 16:00:06 +01:00
parent 017093a8b5
commit 7c613daeda
5 changed files with 157 additions and 5 deletions

View File

@@ -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<String, Object> 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<String, Object> 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;
}
}
}

View File

@@ -162,7 +162,7 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
@Override
public boolean isCommitted() {
return true;
return false;
}
@Override

View File

@@ -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

View File

@@ -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/");

View File

@@ -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 {
// ...
}
}