Best practices of handling PageNotFound for unmapped request-mappings - spring-mvc

My use-case:
I have multiple "kind of logical parts" in my application, that are separated by url. something like:
- someUrl/servletPath/onePartOfMyApplication/...
- someUrl/servletPath/otherPartOfMyApplication/...
Now I want to handle unmapped requests (404s) for each part differently.
How I'm handling it now:
my web.xml:
...
<error-page>
<error-code>404</error-code>
<location>/servletPath/404.html</location>
</error-page>
my controller:
#Controller
public class ExceptionController
{
#ResponseStatus(value = HttpStatus.NOT_FOUND)
#RequestMapping(value = "/404.html")
protected String show404Page(final HttpServletRequest request)
{
final String forward = (String) request.getAttribute("javax.servlet.forward.request_uri");
// parse string and redirect to whereever, depending on context
final String redirectPath = parse(forward);
return "redirect: " + redirectPath;
}
...
My aim:
Is there a more elegant (spring-like)-way of handling 404s, instead of parsing the request in a controller or interceptor and declaring the error-page in my web.xml?
Would be nice if my controller should could look something like this:
#Controller
public class ExceptionController
{
#ResponseStatus(value = HttpStatus.NOT_FOUND)
#RequestMapping(value = "/onePartOfMyApplication/404.html")
protected String show404PageForOnePart(final HttpServletRequest request)
{
// do something
...
return "onePartPage";
}
#ResponseStatus(value = HttpStatus.NOT_FOUND)
#RequestMapping(value = "/otherPartOfMyApplication/404.html")
protected String show404PageForOtherPart(final HttpServletRequest request)
{
// do something different
...
return "otherPartPage";
}

I use #ExceptionHandler annotation. In controller I have something like:
private class ItemNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public ItemNotFoundException(String message) {
super(message);
}
}
#ExceptionHandler
#ResponseStatus(HttpStatus.NOT_FOUND)
public void handleINFException(ItemNotFoundException ex) {
}
And then I throw an exception either in Controller (or in Service layer):
#RequestMapping("/{id}")
#ResponseBody
public Item detail(#PathVariable int id) {
Item item = itemService.findOne(id);
if (item == null) { throw new ItemNotFoundException("Item not found!"); }
return item;
}
You can do anything you like in method annotated with #ExceptionHandler. Right now in my example it shows a standard 404 error which you can customize in web.xml, but you can do much, much more. See documentation: http://docs.spring.io/spring/docs/3.1.x/javadoc-api/org/springframework/web/bind/annotation/ExceptionHandler.html

Related

Springboot app ResponseBodyAdvice Not triggered

I am trying to update an audit entry using the response body advice but as far as I can tell it never gets executed. I see the bean in the logs:
{"timestamp":"2018-08-21T15:48:08.349Z","level":"INFO","thread":"main",
"logger":"org.springframework.data.rest.webmvc.RepositoryRestHandlerAdapter",
"message":"Detected ResponseBodyAdvice bean in responseAuditAdvice","context":"default"}
My controller method looks like this:
#PostMapping(path = "/stage", consumes = {
"application/json"
}, produces = {
"application/json"
})
#ResponseBody
public ResponseEntity<?> stage(#Valid #RequestBody StagingDto stagingDto,
#RequestHeader(HttpHeaders.USER_AGENT) String userAgent,
BindingResult bindingResult) {
I have a RequestAuditAdvice that extends RequestBodyAdviceAdapter and it is working fine. Also if the error flow occurs I see the exception advice executing as well. it is only the response advice that is failing to trigger. Any suggestions?
here is the advice bean:
#Slf4j
#RequiredArgsConstructor(onConstructor_ = #Inject)
#ControllerAdvice
public class ResponseAuditAdvice implements ResponseBodyAdvice<Object> {
private final RequestService requestService;
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
log.info("Updating audit for response.");
String ip = new String (request.getRemoteAddress().getAddress().getAddress());
requestService.auditResponse(ip, 200);
return body;
}
}

Return 404 for every null response

I'd like to return 404 when the response object is null for every response automatically in spring boot.
I need suggestions.
I don't want to check object in controller that it is null or not.
You need more than one Spring module to accomplish this. The basic steps are:
Declare an exception class that can be used to throw an exception when a repository method does not return an expected value.
Add a #ControllerAdvice that catches the custom exception and translates it into an HTTP 404 status code.
Add an AOP advice that intercepts return values of repository methods and raises the custom exception when it finds the values not matching expectations.
Step 1: Exception class
public class ResourceNotFoundException extends RuntimeException {}
Step 2: Controller advice
#ControllerAdvice
public class ResourceNotFoundExceptionHandler
{
#ExceptionHandler(ResourceNotFoundException.class)
#ResponseStatus(HttpStatus.NOT_FOUND)
public void handleResourceNotFound() {}
}
Step 3: AspectJ advice
#Aspect
#Component
public class InvalidRepositoryReturnValueAspect
{
#AfterReturning(pointcut = "execution(* org.example.data.*Repository+.findOne(..))", returning = "result")
public void intercept(final Object result)
{
if (result == null)
{
throw new ResourceNotFoundException();
}
}
}
A sample application is available on Github to demonstrate all of this in action. Use a REST client like Postman for Google Chrome to add some records. Then, attempting to fetch an existing record by its identifier will return the record correctly but attempting to fetch one by a non-existent identifier will return 404.
Simplest way to do this in Spring is write your own exception class like below
#ResponseStatus(value = HttpStatus.NOT_FOUND)
class ResourceNotFoundException extends RuntimeException{
}
Then just throw the ResourceNotFoundException from anywhere.
if (something == null) throw new ResourceNotFoundException();
For more -> Read
Similar to #manish's answer (https://stackoverflow.com/a/43891952/986160) but without the AspectJ pointcut and using another #ControllerAdvice instead:
Step 1: NotFoundException class:
public class NotFoundException extends RuntimeException {
public NotFoundException(String msg) {
super(msg);
}
public NotFoundException() {}
}
Step 2: Check if body returned in endpoint is null and throw NotFoundException:
#ControllerAdvice
public class NotFoundAdvice implements ResponseBodyAdvice {
#Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
#Override
#SuppressWarnings("unchecked")
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body == null) {
throw new NotFoundException("resource not found");
}
return body;
}
}
Step 3: handle NotFoundException and make the response have a status of 404
#ControllerAdvice
public class GlobalExceptionAdvice {
#Data
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
super();
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
}
#ExceptionHandler(NotFoundException.class)
public final ResponseEntity<ErrorDetails> notFoundHandler(Exception ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(),
request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
}
Alternative to Step 3:
You can just annotate your NotFoundException with #ResponseStatus and override fillInStackTrace() (from https://stackoverflow.com/a/31263942/986160) so that it has similar effect to GlobalExceptionAdvice and doesn't show stacktrace like this:
#ResponseStatus(value = HttpStatus.NOT_FOUND,reason = "resource not found")
public class NotFoundException extends RuntimeException {
public NotFoundException(String msg) {
super(msg);
}
public NotFoundException() {}
#Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}
There is no need to throw exceptions, now ResponseBodyAdvice does the trick:
#ControllerAdvice
public class NullTo404 implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
ServerHttpResponse response) {
if (body == null) {
response.setStatusCode(HttpStatus.NOT_FOUND);
}
return body;
}
}
Similarly, you can implement ResponseBodyAdvice<Optional<?>>, and check for Optional.isEmpty() before setting the response status. It has the added benefit of working nicely with CrudRepository. Most controller methods eventually ends like this:
public Optional<Product> getProductBySku(#PathVariable String sku) {
// logic goes here...
return productRepository.findBySku(sku);
}

Inconsistent auto-decoding with spring #RequestParam

I have a normal spring #Controller which takes an URL-encoded string as parameter:
#RequestMapping(value = "/wechat/browser", method = GET)
public void askWeChatWhoTheUserIs(#RequestParam(name = "origin") String origin,
HttpServletResponse response) throws IOException {
//omitted codes
}
When I debug the spring boot application and test the endpoint with browser:
curl http://localhost:8080/wechat/browser\?origin\=http%3A%2F%2Fwww.example.com%2Findex.html%3Fa%3Db%23%2Froute
The origin got decoded automatically and equal to http://www.example.com/index.html?a=b#/route
But when I wrote a spring mvc test:
#RunWith(SpringRunner.class)
#WebMvcTest(WeChatOauthController.class)
public class WeChatOauthControllerTest {
#Autowired
private MockMvc mvc;
#Test
public void itShouldRedirectToWeChatToFinishOauthProtocol() throws Exception {
String origin = "http://www.example.com/index.html?a=b#/route";
String encodedOrigin = URLEncoder.encode(origin, "UTF-8");
this.mvc.perform(get("/wechat/browser")
.param("origin", encodedOrigin))
.andDo(print())
//omitted codes
}
}
When I debug this test and the controller, the origin was not decoded this time. Just wondering why it behaves differently in these two cases.
When supplying a request parameter with the Spring MVC Test framework, there is no need to manually encode the parameter's value since there is no physical HTTP request.
So, just use the original raw value in your test, and it should work fine.
In other words, use this:
#RunWith(SpringRunner.class)
#WebMvcTest(WeChatOauthController.class)
public class WeChatOauthControllerTest {
#Autowired
private MockMvc mvc;
#Test
public void itShouldRedirectToWeChatToFinishOauthProtocol() throws Exception {
this.mvc.perform(get("/wechat/browser")
.param("origin", "http://www.example.com/index.html?a=b#/route"))
.andDo(print())
//omitted codes
}
}
You can use this method , thus there will be proper decoding
#RunWith(SpringRunner.class)
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class WeChatOauthControllerTest {
#LocalServerPort
private int port;
TestRestTemplate restTemplate = new TestRestTemplate();
#Test
public void testAmpersandEncoded(){
ResponseEntity<String> response =
restTemplate.exchange(createURI("%26"),HttpMethod.GET, null, String.class);
assertEquals(response.getStatusCode(), HttpStatus.OK);
}
private URI createURI(String param){
URI uri = null;
String url = "http://localhost:"+ port +"/location?query=" + param;
try {
uri = new URI(url);
} catch (URISyntaxException e) {
log.error(e.getMessage());
}
return uri;
}

advise controller method *before* #Valid annotation is handled

I am adding rate-limiting to a restful webservice using Spring MVC 4.1.
I created a #RateLimited annotation that I can apply to controller methods. A Spring AOP aspect intercepts calls to these methods and throws an exception if there have been too many requests:
#Aspect
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class RateLimitingAspect {
#Autowired
private RateLimitService rateLimitService;
#Before("execution(* com.example..*.*(.., javax.servlet.ServletRequest+, ..)) " +
"&& #annotation(com.example.RateLimited)")
public void wait(JoinPoint jp) throws Throwable {
ServletRequest request =
Arrays
.stream(jp.getArgs())
.filter(Objects::nonNull)
.filter(arg -> ServletRequest.class.isAssignableFrom(arg.getClass()))
.map(ServletRequest.class::cast)
.findFirst()
.get();
String ip = request.getRemoteAddr();
int secondsToWait = rateLimitService.secondsUntilNextAllowedAttempt(ip);
if (secondsToWait > 0) {
throw new TooManyRequestsException(secondsToWait);
}
}
This all works perfectly, except when the #RateLimited controller method has parameters marked as #Valid, e.g.:
#RateLimited
#RequestMapping(method = RequestMethod.POST)
public HttpEntity<?> createAccount(
HttpServletRequest request,
#Valid #RequestBody CreateAccountRequestDto dto) {
...
}
The problem: if validation fails, the validator throws MethodArgumentNotValidException, which is handled by an #ExceptionHandler, which returns an error response to the client, never triggering my #Before and therefore bypassing the rate-limiting.
How can I intercept a web request like this in a way that takes precedence over parameter validation?
I've thought of using Spring Interceptors or plain servlet Filters, but they are mapped by simple url-patterns and I need to differentiate by GET/POST/PUT/etc.
I eventually gave up on trying to find an AOP solution and created a Spring Interceptor instead. The interceptor preHandles all requests and watches for requests whose handler is #RateLimited.
#Component
public class RateLimitingInterceptor extends HandlerInterceptorAdapter {
#Autowired
private final RateLimitService rateLimitService;
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (HandlerMethod.class.isAssignableFrom(handler.getClass())) {
rateLimit(request, (HandlerMethod)handler);
}
return super.preHandle(request, response, handler);
}
private void rateLimit(HttpServletRequest request, HandlerMethod handlerMethod) throws TooManyRequestsException {
if (handlerMethod.getMethodAnnotation(RateLimited.class) != null) {
String ip = request.getRemoteAddr();
int secondsToWait = rateLimitService.secondsUntilNextAllowedInvocation(ip);
if (secondsToWait > 0) {
throw new TooManyRequestsException(secondsToWait);
} else {
rateLimitService.recordInvocation(ip);
}
}
}
}
Add the following controller advice in your application.
#ControllerAdvice
public class ApplicationControllerAdvice {
#InitBinder
#RateLimited
protected void activateBeanPropertyAccess(DataBinder dataBinder) {
dataBinder.initBeanPropertyAccess();
}
}
The #RateLimited should call the class RateLimitingAspect. So, after this all the constraints validator will be called.
See if it's feasible for you to implement similar logic for ##AfterThrowing advice as well which will have similar pointcut.

How should I add attribute after redirect to a controller

I am just a newbie about Spring.
I am now using #ExceptionHandler to handle all the exception for my web application. And after I catch the exception, it will go to and error.jsp page displaying the error message.
I have a ParentController and in that, I have:
#org.springframework.web.bind.annotation.ExceptionHandler(PortalException.class)
public ModelAndView handle(PortalException e, HttpServletRequest request) {
ModelMap map = new ModelMap();
map.addAttribute("message", e.getMessage());
return new ModelAndView("/error", map);
}
and I have a ErrorControllerextends the ParentController to add the attributes:
#Controller
public class ErrorController extends ParentSecureController {
#RequestMapping(value = "/error", method = RequestMethod.POST)
#ResponseBody
public String errorHandler(Model model, HttpServletRequest request) {
model.addAttribute("excetpion.message", request.getParameter("message"));
return "/error";
}
}
In the error.jsp:
<p>Excpetion is: ${exception.message}</p>
When I run my application, I can catch the exception and jump to error.jsp, but no exception message is display.
Anyone can help me to figure out how to solve it.
Please try use:
#Controller
public class ErrorController extends ParentSecureController {
#RequestMapping(value = "/error", method = RequestMethod.POST)
#ResponseBody
public String errorHandler(Map<String, Object> map, HttpServletRequest request) {
map.put("excetpion.message", request.getParameter("message"));
return "/error";
}
}
UPDATE
Map you get it messae from #Controller to View in this case error.jsp
I hope these helped! :)

Resources