Add initial support for error handling
This commit is contained in:
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -162,7 +162,7 @@ public class ProxyHttpServletResponse implements HttpServletResponse {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isCommitted() {
|
public boolean isCommitted() {
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import java.util.List;
|
|||||||
import javax.servlet.Filter;
|
import javax.servlet.Filter;
|
||||||
import javax.servlet.FilterChain;
|
import javax.servlet.FilterChain;
|
||||||
import javax.servlet.FilterConfig;
|
import javax.servlet.FilterConfig;
|
||||||
|
import javax.servlet.RequestDispatcher;
|
||||||
import javax.servlet.Servlet;
|
import javax.servlet.Servlet;
|
||||||
import javax.servlet.ServletConfig;
|
import javax.servlet.ServletConfig;
|
||||||
import javax.servlet.ServletContext;
|
import javax.servlet.ServletContext;
|
||||||
@@ -36,10 +37,15 @@ import javax.servlet.ServletResponse;
|
|||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
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.context.annotation.AnnotatedBeanDefinitionReader;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.lang.Nullable;
|
import org.springframework.lang.Nullable;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
import org.springframework.web.context.ConfigurableWebApplicationContext;
|
||||||
import org.springframework.web.context.support.GenericWebApplicationContext;
|
import org.springframework.web.context.support.GenericWebApplicationContext;
|
||||||
import org.springframework.web.servlet.DispatcherServlet;
|
import org.springframework.web.servlet.DispatcherServlet;
|
||||||
@@ -55,6 +61,8 @@ import org.springframework.web.servlet.DispatcherServlet;
|
|||||||
*/
|
*/
|
||||||
public class ProxyMvc {
|
public class ProxyMvc {
|
||||||
|
|
||||||
|
private static Log LOG = LogFactory.getLog(ProxyMvc.class);
|
||||||
|
|
||||||
static final String MVC_RESULT_ATTRIBUTE = ProxyMvc.class.getName().concat(".MVC_RESULT_ATTRIBUTE");
|
static final String MVC_RESULT_ATTRIBUTE = ProxyMvc.class.getName().concat(".MVC_RESULT_ATTRIBUTE");
|
||||||
|
|
||||||
private final DispatcherServlet servlet;
|
private final DispatcherServlet servlet;
|
||||||
@@ -71,6 +79,8 @@ public class ProxyMvc {
|
|||||||
AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(applpicationContext);
|
AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(applpicationContext);
|
||||||
reader.register(componentClasses);
|
reader.register(componentClasses);
|
||||||
|
|
||||||
|
reader.register(ProxyErrorController.class);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
DispatcherServlet servlet = new DispatcherServlet(applpicationContext);
|
DispatcherServlet servlet = new DispatcherServlet(applpicationContext);
|
||||||
servlet.init(new ProxyServletConfig(servletContext));
|
servlet.init(new ProxyServletConfig(servletContext));
|
||||||
@@ -199,7 +209,40 @@ public class ProxyMvc {
|
|||||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||||
throws IOException, ServletException {
|
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
|
@Override
|
||||||
|
|||||||
@@ -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.Pet;
|
||||||
import org.springframework.cloud.function.test.app.PetStoreSpringAppConfig;
|
import org.springframework.cloud.function.test.app.PetStoreSpringAppConfig;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ public class RequestResponseTests {
|
|||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
public void before() {
|
public void before() {
|
||||||
this.mvc = ProxyMvc.INSTANCE(PetStoreSpringAppConfig.class);
|
this.mvc = ProxyMvc.INSTANCE(PetStoreSpringAppConfig.class, ProxyErrorController.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterEach
|
@AfterEach
|
||||||
@@ -87,6 +88,23 @@ public class RequestResponseTests {
|
|||||||
assertThat(pet.getName()).isNotEmpty();
|
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
|
@Test
|
||||||
public void validatePostWithBody() throws Exception {
|
public void validatePostWithBody() throws Exception {
|
||||||
ProxyHttpServletRequest request = new ProxyHttpServletRequest(null, "POST", "/pets/");
|
ProxyHttpServletRequest request = new ProxyHttpServletRequest(null, "POST", "/pets/");
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ import java.security.Principal;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
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.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
import org.springframework.web.bind.annotation.RequestMethod;
|
||||||
import org.springframework.web.bind.annotation.RequestParam;
|
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.bind.annotation.RestController;
|
||||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||||
|
|
||||||
@@ -63,8 +66,10 @@ public class PetsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET)
|
@RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET)
|
||||||
public Pet listPets() {
|
public Pet listPets(@PathVariable String petId) {
|
||||||
System.out.println("=====> Getting pet by id");
|
if (petId.equals("2")) {
|
||||||
|
throw new DogNotFoundException();
|
||||||
|
}
|
||||||
Pet newPet = new Pet();
|
Pet newPet = new Pet();
|
||||||
newPet.setId(UUID.randomUUID().toString());
|
newPet.setId(UUID.randomUUID().toString());
|
||||||
newPet.setBreed(PetData.getRandomBreed());
|
newPet.setBreed(PetData.getRandomBreed());
|
||||||
@@ -72,4 +77,9 @@ public class PetsController {
|
|||||||
newPet.setName(PetData.getRandomName());
|
newPet.setName(PetData.getRandomName());
|
||||||
return newPet;
|
return newPet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "No such Dog") // 404
|
||||||
|
public class DogNotFoundException extends RuntimeException {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user