How to read HTTPRequest in Spring Boot Exception Handler?

Let’s say you want to centralize exception handling in your spring boot application.

Whenever any of your APIs throw exception you want to handle them in a centralized way.

How can you do this?

Using Spring’s Exception Handler.

You create a class annotated with @ControllerAdvice annotation and write a method annotated with @ExceptionHandler .

You mention what exception needs to be handled by the method by passing it as a parameter value.

Inside this method you catch the exception and return a custom error response to the user.

This works fine as long as you don’t want to read the actual request sent by the user.

Why?

Because when you try to read the requests (for logging it etc) in an exception handler Spring complains that the request has already been read. HttpServletRequest can be read only once in a web application and this causes the error.

Let’s see how to handle this.

Here are the steps:

STEP1 : Create a Controller Advice class

STEP2: Create an exception handler method to handle specific exception

STEP3: Throw the exception in Rest Controller

STEP4: Create a HTTPServletRequest Wrapper

STEP5: Create a Servlet Filter which filters incoming requests and wraps them with the HttpServletRequestWrapper object

By following the last two steps you can log your incoming request or process it without Spring complaining that the request has already been read.

In this case you read the wrapped object and not the actual HTTPServletRequest object.

Here is the implementation in detail:

STEP1 : Create a Controller Advice class

Let’s create a controller advice class:

package com.exception.handler.exceptiondemo;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.databind.ObjectMapper;

@ControllerAdvice
public class CustomExceptionHandler {


	
	
		
}

STEP2 : Create an exception handler method

Inside the controller advice class created include an exception handler.

In the below example , I have created an exception handler method to handle any exception:

Note the annotations @ExceptionHandler and @ResponseBody (this is required to convert the error response to JSON and return as HTTPResponse object)

package com.exception.handler.exceptiondemo;

import java.util.HashMap;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.databind.ObjectMapper;

@ControllerAdvice
public class CustomExceptionHandler {

	Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);
	
	@ExceptionHandler(value = Exception.class)
	@ResponseBody
	public Map<String,String> handleException(Exception e, HttpServletRequest request){
		
		
		Map<String, Object> requestMap = null;
		try {
			requestMap = new ObjectMapper().readValue(request.getInputStream(), Map.class);
		} catch (Exception e1) {
			e1.printStackTrace();
		}
		
		logger.error("Exception occured for the request:"+requestMap);
		logger.error("Exception is "+e.getMessage());
		
		Map<String,String> response = new HashMap();
		
		response.put("status", "failure");
		
		return response;
		
	}
}

STEP 3: Throw exception in Rest Controller

Let’s create a rest controller class with a single API.

Let’s say our application is a shop that sells items and it gets closed during lunch hours.

So whenever user tries to buy during lunch hours we throw an Exception:

package com.exception.handler.exceptiondemo;

import java.time.LocalDateTime;
import java.util.Map;

import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@org.springframework.web.bind.annotation.RestController
public class RestController {

	@PostMapping(value = "/buy")
	@CrossOrigin
	public Map<String, Object> buy(@RequestBody Map<String, Object> request) throws Exception {

		
		if(LocalDateTime.now().getHour() > 13 && LocalDateTime.now().getHour() < 15) {
			
			throw new Exception("Shop is closed for lunch");
		}
		
		return request;
	}

}

Before moving to the next steps , let’s check if we are able to read the http request object and log it.

I tried hitting the request through postman during lunch hours and got the below error response :

But I could not log the request because Spring complained that the input stream is already closed:

2020-09-28 14:06:08.887 INFO 20404 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Completed initialization in 11 ms
java.io.IOException: Stream closed
at org.apache.catalina.connector.InputBuffer.read(InputBuffer.java:359)
at org.apache.catalina.connector.CoyoteInputStream.read(CoyoteInputStream.java:132)
at com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.ensureLoaded(ByteSourceJsonBootstrapper.java:524)
at com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.detectEncoding(ByteSourceJsonBootstrapper.java:129)
at com.fasterxml.jackson.core.json.ByteSourceJsonBootstrapper.constructParser(ByteSourceJsonBootstrapper.java:247)
at com.fasterxml.jackson.core.JsonFactory._createParser(JsonFactory.java:1485)
at com.fasterxml.jackson.core.JsonFactory.createParser(JsonFactory.java:972)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3503)
at com.exception.handler.exceptiondemo.CustomExceptionHandler.handleException(CustomExceptionHandler.java:28)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:105)
at org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver.doResolveHandlerMethodException(ExceptionHandlerExceptionResolver.java:407)
at org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver.doResolveException(AbstractHandlerMethodExceptionResolver.java:61)
at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:141)
at org.springframework.web.servlet.handler.HandlerExceptionResolverComposite.resolveException(HandlerExceptionResolverComposite.java:80)
at org.springframework.web.servlet.DispatcherServlet.processHandlerException(DispatcherServlet.java:1300)
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1111)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1057)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:943)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:652)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:883)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:733)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:119)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:202)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:97)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:541)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:143)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:78)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:374)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:65)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:868)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1590)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.base/java.lang.Thread.run(Thread.java:832)
2020-09-28 14:06:08.976 ERROR 20404 --- [nio-8080-exec-2] c.e.h.e.CustomExceptionHandler : Exception occured for the request:null
2020-09-28 14:06:08.977 ERROR 20404 --- [nio-8080-exec-2] c.e.h.e.CustomExceptionHandler : Exception is Shop is closed for lunch

To resolve this , you need to follow two more steps.

STEP 4: Create a HTTPServletRequestWrapper object

Create a wrapper object to wrap http requests by extending HttpServletRequestWrapper class.

Here is the implementation :

package com.exception.handler.exceptiondemo;

import java.io.IOException;
import java.io.StringReader;
import java.util.Scanner;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;


public class RequestWrapper extends HttpServletRequestWrapper {

	private String requestData = null;

	public RequestWrapper(HttpServletRequest request) {
		
		super(request);

		try (Scanner s = new Scanner(request.getInputStream()).useDelimiter("\\A")) {

			requestData = s.hasNext() ? s.next() : "";

		} catch (IOException e) {
			e.printStackTrace();
		}

	}

	@Override
	public ServletInputStream getInputStream() throws IOException {

		StringReader reader = new StringReader(requestData);

		return new ServletInputStream() {

			private ReadListener readListener = null;

			@Override
			public int read() throws IOException {

				return reader.read();
			}

			@Override
			public void setReadListener(ReadListener listener) {
				this.readListener = listener;

				try {
					if (!isFinished()) {

						readListener.onDataAvailable();
					} else {

						readListener.onAllDataRead();
					}
				} catch (IOException io) {

					io.printStackTrace();
				}

			}

			@Override
			public boolean isReady() {

				return isFinished();
			}

			@Override
			public boolean isFinished() {

				try {
					return reader.read() < 0;
				} catch (IOException e) {
					e.printStackTrace();
				}

				return false;

			}
		};
	}

}

You need to do two things here:

  1. Extract data out of the HttpServletRequest object and convert it to a string in a constructor (I have used Scanner class to do this)
  2. Override getInputStream() method and return a custom ServletInputStream which contains the data extracted in the constructor.

STEP5 : Create a Servlet filter

Create a servlet filter which will filter incoming http requests and wrap them inside the wrapper object created above.

package com.exception.handler.exceptiondemo;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

import org.springframework.stereotype.Component;

@Component
public class RequestFilter implements Filter {

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest wrappedRequest = new RequestWrapper((HttpServletRequest) request);

		chain.doFilter(wrappedRequest, response);

	}

}

As you see the httpservletrequest object is wrapped inside HttpServletRequestWrapper object and passed to the filter chain.

Now if you test your application , you will be able to see the requests logged:

2020-09-28 14:04:52.051 ERROR 16032 --- [nio-8080-exec-2] c.e.h.e.CustomExceptionHandler : Exception occured for the request:{items={shoes=1, socks=1}}
2020-09-28 14:04:52.051 ERROR 16032 --- [nio-8080-exec-2] c.e.h.e.CustomExceptionHandler : Exception is Shop is closed for lunch

That’s it!

Here is the entire code:

https://github.com/vijaysrj/exceptiondemo

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s