Reading httprequest content from spring exception handler - java

I Am using Spring's #ExceptionHandler annotation to catch exceptions in my controllers.
Some requests hold POST data as plain XML string written to the request body, I want to read that data in order to log the exception.
The problem is that when i request the inputstream in the exception handler and try to read from it the stream returns -1 (empty).
The exception handler signature is:
#ExceptionHandler(Throwable.class)
public ModelAndView exception(HttpServletRequest request, HttpServletResponse response, HttpSession session, Throwable arff)
Any thoughts? Is there a way to access the request body?
My controller:
#Controller
#RequestMapping("/user/**")
public class UserController {
static final Logger LOG = LoggerFactory.getLogger(UserController.class);
#Autowired
IUserService userService;
#RequestMapping("/user")
public ModelAndView getCurrent() {
return new ModelAndView("user","response", userService.getCurrent());
}
#RequestMapping("/user/firstLogin")
public ModelAndView firstLogin(HttpSession session) {
userService.logUser(session.getId());
userService.setOriginalAuthority();
return new ModelAndView("user","response", userService.getCurrent());
}
#RequestMapping("/user/login/failure")
public ModelAndView loginFailed() {
LOG.debug("loginFailed()");
Status status = new Status(-1,"Bad login");
return new ModelAndView("/user/login/failure", "response",status);
}
#RequestMapping("/user/login/unauthorized")
public ModelAndView unauthorized() {
LOG.debug("unauthorized()");
Status status = new Status(-1,"Unauthorized.Please login first.");
return new ModelAndView("/user/login/unauthorized","response",status);
}
#RequestMapping("/user/logout/success")
public ModelAndView logoutSuccess() {
LOG.debug("logout()");
Status status = new Status(0,"Successful logout");
return new ModelAndView("/user/logout/success", "response",status);
}
#RequestMapping(value = "/user/{id}", method = RequestMethod.POST)
public ModelAndView create(#RequestBody UserDTO userDTO, #PathVariable("id") Long id) {
return new ModelAndView("user", "response", userService.create(userDTO, id));
}
#RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
public ModelAndView getUserById(#PathVariable("id") Long id) {
return new ModelAndView("user", "response", userService.getUserById(id));
}
#RequestMapping(value = "/user/update/{id}", method = RequestMethod.POST)
public ModelAndView update(#RequestBody UserDTO userDTO, #PathVariable("id") Long id) {
return new ModelAndView("user", "response", userService.update(userDTO, id));
}
#RequestMapping(value = "/user/all", method = RequestMethod.GET)
public ModelAndView list() {
return new ModelAndView("user", "response", userService.list());
}
#RequestMapping(value = "/user/allowedAccounts", method = RequestMethod.GET)
public ModelAndView getAllowedAccounts() {
return new ModelAndView("user", "response", userService.getAllowedAccounts());
}
#RequestMapping(value = "/user/changeAccount/{accountId}", method = RequestMethod.GET)
public ModelAndView changeAccount(#PathVariable("accountId") Long accountId) {
Status st = userService.changeAccount(accountId);
if (st.code != -1) {
return getCurrent();
}
else {
return new ModelAndView("user", "response", st);
}
}
/*
#RequestMapping(value = "/user/logout", method = RequestMethod.GET)
public void perLogout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
userService.setOriginalAuthority();
response.sendRedirect("/marketplace/user/logout/spring");
}
*/
#ExceptionHandler(Throwable.class)
public ModelAndView exception(HttpServletRequest request, HttpServletResponse response, HttpSession session, Throwable arff) {
Status st = new Status();
try {
Writer writer = new StringWriter();
byte[] buffer = new byte[1024];
//Reader reader2 = new BufferedReader(new InputStreamReader(request.getInputStream()));
InputStream reader = request.getInputStream();
int n;
while ((n = reader.read(buffer)) != -1) {
writer.toString();
}
String retval = writer.toString();
retval = "";
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView("profile", "response", st);
}
}
Thank you

I've tried your code and I've found some mistakes in the exception handler, when you read from the InputStream:
Writer writer = new StringWriter();
byte[] buffer = new byte[1024];
//Reader reader2 = new BufferedReader(new InputStreamReader(request.getInputStream()));
InputStream reader = request.getInputStream();
int n;
while ((n = reader.read(buffer)) != -1) {
writer.toString();
}
String retval = writer.toString();
retval = "";
I've replaced your code with this one:
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
String line = "";
StringBuilder stringBuilder = new StringBuilder();
while ( (line=reader.readLine()) != null ) {
stringBuilder.append(line).append("\n");
}
String retval = stringBuilder.toString();
Then I'm able to read from InputStream in the exception handler, it works!
If you can't still read from InputStream, I suggest you to check how you POST xml data to the request body.
You should consider that you can consume the Inputstream only one time per request, so I suggest you to check that there isn't any other call to getInputStream(). If you have to call it two or more times you should write a custom HttpServletRequestWrapper like this to make a copy of the request body, so you can read it more times.
UPDATE
Your comments has helped me to reproduce the issue. You use the annotation #RequestBody, so it's true that you don't call getInputStream(), but Spring invokes it to retrieve the request's body. Have a look at the class org.springframework.web.bind.annotation.support.HandlerMethodInvoker: if you use #RequestBody this class invokes resolveRequestBody method, and so on... finally you can't read anymore the InputStream from your ServletRequest. If you still want to use both #RequestBody and getInputStream() in your own method, you have to wrap the request to a custom HttpServletRequestWrapper to make a copy of the request body, so you can manually read it more times.
This is my wrapper:
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final Logger logger = Logger.getLogger(CustomHttpServletRequestWrapper.class);
private final String body;
public CustomHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line = "";
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
logger.error("Error reading the request body...");
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
logger.error("Error closing bufferedReader...");
}
}
}
body = stringBuilder.toString();
}
#Override
public ServletInputStream getInputStream() throws IOException {
final StringReader reader = new StringReader(body);
ServletInputStream inputStream = new ServletInputStream() {
public int read() throws IOException {
return reader.read();
}
};
return inputStream;
}
}
Then you should write a simple Filter to wrap the request:
public class MyFilter implements Filter {
public void init(FilterConfig fc) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(new CustomHttpServletRequestWrapper((HttpServletRequest)request), response);
}
public void destroy() {
}
}
Finally, you have to configure your filter in your web.xml:
<filter>
<filter-name>MyFilter</filter-name>
<filter-class>test.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>MyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
You can fire your filter only for controllers that really needs it, so you should change the url-pattern according to your needs.
If you need this feature in only one controller, you can also make a copy of the request body in that controller when you receive it through the #RequestBody annotation.

Recently I faced this issue and solved it slightly differently. With spring boot 1.3.5.RELEASE
The filter was implemented using the Spring class ContentCachingRequestWrapper. This wrapper has a method getContentAsByteArray() which can be invoked multiple times.
import org.springframework.web.util.ContentCachingRequestWrapper;
public class RequestBodyCachingFilter implements Filter {
public void init(FilterConfig fc) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(new ContentCachingRequestWrapper((HttpServletRequest)request), response);
}
public void destroy() {
}
}
Added the filter to the chain
#Bean
public RequestBodyCachingFilter requestBodyCachingFilter() {
log.debug("Registering Request Body Caching filter");
return new RequestBodyCachingFilter();
}
In the Exception Handler.
#ControllerAdvice(annotations = RestController.class)
public class GlobalExceptionHandlingControllerAdvice {
private ContentCachingRequestWrapper getUnderlyingCachingRequest(ServletRequest request) {
if (ContentCachingRequestWrapper.class.isAssignableFrom(request.getClass())) {
return (ContentCachingRequestWrapper) request;
}
if (request instanceof ServletRequestWrapper) {
return getUnderlyingCachingRequest(((ServletRequestWrapper)request).getRequest());
}
return null;
}
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
#ExceptionHandler(Throwable.class)
public #ResponseBody Map<String, String> conflict(Throwable exception, HttpServletRequest request) {
ContentCachingRequestWrapper underlyingCachingRequest = getUnderlyingCachingRequest(request);
String body = new String(underlyingCachingRequest.getContentAsByteArray(),Charsets.UTF_8);
....
}
}

I had the same problem and solved it with HttpServletRequestWrapper as described above and it worked great. But then, I found another solution with extending HttpMessageConverter, in my case that was MappingJackson2HttpMessageConverter.
public class CustomJsonHttpMessageConverter extends MappingJackson2HttpMessageConverter{
public static final String REQUEST_BODY_ATTRIBUTE_NAME = "key.to.requestBody";
#Override
public Object read(Type type, Class<?> contextClass, final HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
final ByteArrayOutputStream writerStream = new ByteArrayOutputStream();
HttpInputMessage message = new HttpInputMessage() {
#Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
#Override
public InputStream getBody() throws IOException {
return new TeeInputStream(inputMessage.getBody(), writerStream);
}
};
RequestContextHolder.getRequestAttributes().setAttribute(REQUEST_BODY_ATTRIBUTE_NAME, writerStream, RequestAttributes.SCOPE_REQUEST);
return super.read(type, contextClass, message);
}
}
com.sun.xml.internal.messaging.saaj.util.TeeInputStream is used.
In spring mvc config
<mvc:annotation-driven >
<mvc:message-converters>
<bean class="com.company.remote.rest.util.CustomJsonHttpMessageConverter" />
</mvc:message-converters>
</mvc:annotation-driven>
In #ExceptionHandler method
#ExceptionHandler(Exception.class)
public ResponseEntity<RestError> handleException(Exception e, HttpServletRequest httpRequest) {
RestError error = new RestError();
error.setErrorCode(ErrorCodes.UNKNOWN_ERROR.getErrorCode());
error.setDescription(ErrorCodes.UNKNOWN_ERROR.getDescription());
error.setDescription(e.getMessage());
logRestException(httpRequest, e);
ResponseEntity<RestError> responseEntity = new ResponseEntity<RestError>(error,HttpStatus.INTERNAL_SERVER_ERROR);
return responseEntity;
}
private void logRestException(HttpServletRequest request, Exception ex) {
StringWriter sb = new StringWriter();
sb.append("Rest Error \n");
sb.append("\nRequest Path");
sb.append("\n----------------------------------------------------------------\n");
sb.append(request.getRequestURL());
sb.append("\n----------------------------------------------------------------\n");
Object requestBody = request.getAttribute(CustomJsonHttpMessageConverter.REQUEST_BODY_ATTRIBUTE_NAME);
if(requestBody != null) {
sb.append("\nRequest Body\n");
sb.append("----------------------------------------------------------------\n");
sb.append(requestBody.toString());
sb.append("\n----------------------------------------------------------------\n");
}
LOG.error(sb.toString());
}
I hope it helps :)

Related

Java Filter modified response is getting truncated

I have a spring java application that has a EncryptDecryptFilter.java which is a "OncePerRequestFilter" within which I am trying to modify the response payload based on certain conditions. All is going well for happy path.
During the un-happy path, when my application code in the RestController throws an exception, that exception is captured by a #ControllerAdvice class and the controller advice class returns a ResponseEntity. Let's say this response is "string1" with a length 105 characters. After this, the call is intercepted by the "OncePerRequestFilter" and when I attempt to modify the response in this filter with a new response, lets say the modified response is "string2" with a length 200 characters, the modified response string2 is truncated to 105 characters and delievered to the client. So, while the response it self is being modified, the setContentLength() on the response has no effect.
Any suggestions on how to fix this?
See response.setContentLength(encryptedResponse.length()); in the below filter
#Component
public class EncryptDecryptFilter extends OncePerRequestFilter {
public static final Gson gson = new GsonBuilder()
.addSerializationExclusionStrategy(new GsonExclusionStrategy())
.setPrettyPrinting()
.create();
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String requestURI = request.getRequestURI();
String requestMethod = request.getMethod();
HttpServletRequest requestToUse = new ResettableStreamHttpServletRequest(request);
BufferResponseWrapper responseToUse = new BufferResponseWrapper(response); // declared as a member of this filter class below
interceptRequest(requestToUse, request);
filterChain.doFilter(requestToUse, responseToUse);
interceptResponse(response, responseToUse);
}
private void interceptRequest(HttpServletRequest requestToUse, HttpServletRequest request) throws IOException {
byte[] payload = IOUtils.toByteArray(requestToUse.getReader(), requestToUse.getCharacterEncoding());
String urlEncodedEncryptedBody = new String(payload, requestToUse.getCharacterEncoding());
System.out.println(urlEncodedEncryptedBody);
AesDecryptionRequest decryptionRequest = gson.fromJson(urlEncodedEncryptedBody, AesDecryptionRequest.class);
decryptionRequest.initialize();
String plainTextBody = AesUtil.decrypt(decryptionRequest);
requestToUse.setAttribute("requestBody", plainTextBody);
}
private void interceptResponse(HttpServletResponse response, BufferResponseWrapper responseToUse) throws IOException {
String responseStr = new String(responseToUse.getWrapperBytes());
System.out.println(responseStr); // full response string with length 105 from #ControllerAdvice
AesEncryptionResponse encryptionResponse = AesUtil.encrypt(responseStr);
String encryptedResponse = gson.toJson(encryptionResponse); // encrypted form of #ControllerAdvice response length 200n chars
response.setContentLength(encryptedResponse.length()); // this does not seem to have any effect, the response is still truncated to 105 chars
response.getOutputStream().write(encryptedResponse.getBytes(StandardCharsets.UTF_8));
}
private final class BufferResponseWrapper extends HttpServletResponseWrapper
{
MyServletOutputStream stream = new MyServletOutputStream();
public BufferResponseWrapper(HttpServletResponse httpServletResponse)
{
super(httpServletResponse);
}
public ServletOutputStream getOutputStream() throws IOException
{
return stream;
}
public PrintWriter getWriter() throws IOException
{
return new PrintWriter(stream);
}
public byte[] getWrapperBytes()
{
return stream.getBytes();
}
}
private final class MyServletOutputStream extends ServletOutputStream
{
private ByteArrayOutputStream out = new ByteArrayOutputStream();
public void write(int b) throws IOException
{
out.write(b);
}
public byte[] getBytes()
{
return out.toByteArray();
}
#Override
public boolean isReady() {
return false;
}
#Override
public void setWriteListener(WriteListener writeListener) {
}
}
}
public class ResettableStreamHttpServletRequest extends HttpServletRequestWrapper {
// this class allows the input stream to be read more than once
private static final String UTF_8 = "UTF-8";
private byte[] rawData;
private HttpServletRequest request;
private ResettableServletInputStream servletStream;
public ResettableStreamHttpServletRequest(HttpServletRequest request) {
super(request);
this.request = request;
this.servletStream = new ResettableServletInputStream();
}
public void resetInputStream() {
servletStream.stream = new ByteArrayInputStream(rawData);
}
public void resetInputStream(byte[] newRawData) {
servletStream.stream = new ByteArrayInputStream(newRawData);
}
#Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
#Override
public BufferedReader getReader() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getReader(), UTF_8);
servletStream.stream = new ByteArrayInputStream(rawData);
}
return new BufferedReader(new InputStreamReader(servletStream, UTF_8));
}
private class ResettableServletInputStream extends ServletInputStream {
private ByteArrayInputStream stream;
#Override
public int read() throws IOException {
return stream.read();
}
#Override
// Returns true when all the data from the stream has been read else it returns false.
public boolean isFinished() {
return stream.available() == 0;
}
#Override
// Returns true if data can be read without blocking else returns false.
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
}
}
Controller Advice class
#ControllerAdvice
public class ExceptionAdvice extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {Exception.class})
protected ResponseEntity<Object> handleException(Exception exception, WebRequest request) {
return handleExceptionInternal(exception, exception.getMessage(), getStandardHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request);
}
}
Try using .reset() method on response object before you rewrite it.
private void interceptResponse(HttpServletResponse response, BufferResponseWrapper responseToUse) throws IOException {
String responseStr = new String(responseToUse.getWrapperBytes());
System.out.println(responseStr); // full response string with length 105 from #ControllerAdvice
AesEncryptionResponse encryptionResponse = AesUtil.encrypt(responseStr);
String encryptedResponse = gson.toJson(encryptionResponse); // encrypted form of #ControllerAdvice response length 200n chars
response.reset(); <-------------
response.setContentLength(encryptedResponse.length()); // this does not seem to have any effect, the response is still truncated to 105 chars
response.getOutputStream().write(encryptedResponse.getBytes(StandardCharsets.UTF_8));
}
According to the doc
void reset() Clears any data that exists in the buffer as well as the
status code and headers. If the response has been committed, this
method throws an IllegalStateException.
Throws: IllegalStateException
if the response has already been committed

Post request data lost after using custom filter and custom requestWrapper in Spring boot

I am writing a Spring boot filter to verify request data before it hits the rest controller. To avoid HttpServletRequest read ServletInputStream twice, I writed a CustomRequestWrapper to wrap it. But When I send a post request(conetentType = "multipart/form-data") by postman , I get a null in rest controller.
Here is my filter:
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
CustomReqeustWrapper requestWrapper = new CustomReqeustWrapper(req);
CustomResponseWrapper responseWrapper = new CustomResponseWrapper(resp);
// get requestBody from requestWrapper and verify.
readContent(requestWrapper);
chain.doFilter(requestWrapper, responseWrapper);
writeContent(response);
}
Here is my RequestWrapper:
public class CustomRequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody = new byte[0];
public CustomRequestWrapper (HttpServletRequest request) {
super(request);
try {
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
#Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
#Override
public int read() throws IOException {
return bais.read();
}
#Override
public boolean isFinished() {
return false;
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener listener) {
}
};
}
public byte[] getRequestData() {
return requestBody;
}
public void setRequestData(byte[] requestData) {
this.requestBody = requestData;
}
}
Here is my controller:
#PostMapping("/exchange")
public ResponseEntity<Void> keyExchange(HttpServletRequest request, String Key) throws Exception {
// get the "key" is null.
}
If the type of parameter 'key' is String,maybe you should't use 'multipart' data type.
For all I know,I had met this problem once.The wrapper is useless for file parameter.
If you have to use 'multipart' type,perhaps you should use other manner to figure out it.

How to read request body in HandlerInterceptor?

I have Spring Boot and I need to log user action in DB, so I wrote HandlerInterceptor:
#Component
public class LogInterceptor implements HandlerInterceptor {
#Autovired
private LogUserActionService logUserActionService;
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws IOException {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
String url = request.getRequestURI();
String queryString = request.getQueryString() != null ? request.getQueryString() : "";
String body = "POST".equalsIgnoreCase(request.getMethod()) ? new BufferedReader(new InputStreamReader(request.getInputStream())).lines().collect(Collectors.joining(System.lineSeparator())) : queryString;
logUserActionService.logUserAction(userName, url, body);
return true;
}
}
But according to this answer Get RequestBody and ResponseBody at HandlerInterceptor "RequestBody can be read only once", so as I understand I read input stream and then Spring tries to do same, but stream has been read already and I'm getting an error: "Required request body is missing ..."
So I tried different ways to make buffered input stream i.e.:
HttpServletRequest httpServletRequest = new ContentCachingRequestWrapper(request);
new BufferedReader(new InputStreamReader(httpServletRequest.getInputStream())).lines().collect(Collectors.joining(System.lineSeparator()))
Or
InputStream bufferedInputStream = new BufferedInputStream(request.getInputStream());
But nothing helped
Also I tried to use
#ControllerAdvice
public class UserActionRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {
But it has only body, no request info like URL or Request parameters
Also tried to use Filters, but result same.
So I need a good way to get information from request like user, URL, parameters, body (if present) and write it to DB.
To log HTTP Request & Response, you can use RequestBodyAdviceAdapter and ResponseBodyAdvice. here, it is using in my way.
CustomRequestBodyAdviceAdapter.java
#ControllerAdvice
public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter {
#Autowired
HttpServletRequest httpServletRequest;
#Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) {
// here you can full log httpServletRequest and body.
return super.afterBodyRead(body, inputMessage, parameter, targetType, converterType);
}
}
CustomResponseBodyAdviceAdapter.java
#ControllerAdvice
public class CustomResponseBodyAdviceAdapter implements ResponseBodyAdvice<Object> {
#Autowired
private LoggingService loggingService;
#Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
#Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if (serverHttpRequest instanceof ServletServerHttpRequest && serverHttpResponse instanceof ServletServerHttpResponse) {
// here you can full log httpServletRequest and body.
}
return o;
}
}
Above AdviceAdapter cannot handle the GET request. So, you can use HandlerInterceptor.
CustomWebConfigurerAdapter.java
#Component
public class CustomWebConfigurerAdapter implements WebMvcConfigurer {
#Autowired
private CustomLogInterceptor httpServiceInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(httpServiceInterceptor);
}
}
CustomLogInterceptor.java
#Component
public class CustomLogInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (DispatcherType.REQUEST.name().equals(request.getDispatcherType().name()) && request.getMethod().equals(HttpMethod.GET.name())) {
// here you can full log httpServletRequest and body for GET Request.
}
return true;
}
}
Here you can reference full source code in my git.
springboot-http-request-response-loging-with-json-logger
+Feature => It is already have Integration with ELK (Elasticsearch, Logstash, Kibana)
You can use Filter to log request body.
public class LoggingFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request);
try {
chain.doFilter(wrappedRequest, res);
} finally {
logRequestBody(wrappedRequest);
}
}
private static void logRequestBody(ContentCachingRequestWrapper request) {
byte[] buf = request.getContentAsByteArray();
if (buf.length > 0) {
try {
String requestBody = new String(buf, 0, buf.length, request.getCharacterEncoding());
System.out.println(requestBody);
} catch (Exception e) {
System.out.println("error in reading request body");
}
}
}
}
The main thing to note here is that you have to pass object of ContentCachingRequestWrapper in filter chain otherwise you won't get request content in it.
In above example, if you use chain.doFilter(req, res) or chain.doFilter(request, res) then you won't get request body in wrappedRequest object.
You can get the Request Body data using RequestBodyAdviceAdapter for POST/PUT requests. You can use HandlerInterceptorAdapter for GET calls. Here's a working example -
https://frandorado.github.io/spring/2018/11/15/log-request-response-with-body-spring.html
#ControllerAdvice
public class CustomRequestBodyAdviceAdapter extends RequestBodyAdviceAdapter
{
#Autowired
HttpServletRequest httpServletRequest;
private static final Log LOGGER = LogFactory.getLog(CustomRequestBodyAdviceAdapter.class);
private static final Charset DEFAULT_CHARSET = ISO_8859_1;
#Override
public boolean supports(MethodParameter methodParameter, Type type,
Class<? extends HttpMessageConverter<?>> aClass)
{
return true;
}
#Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType)
{
Instant startTime = Instant.now();
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("REQUEST call Starts :: Start Time : %s ").append(startTime);
try
{
logRequest(httpServletRequest, body);
}
catch (IOException e)
{
LOGGER.info("Exception getting the Request Body into the Log: {}" + e.getMessage());
}
public void logRequest(HttpServletRequest httpServletRequest, Object body) throws IOException
{
StringBuilder stringBuilder = new StringBuilder();
Map<String, String> parameters = buildParametersMap(httpServletRequest);
stringBuilder.append("REQUEST ");
stringBuilder.append("method=[").append(httpServletRequest.getMethod()).append("] ");
stringBuilder.append("path=[").append(httpServletRequest.getRequestURI()).append("] ");
stringBuilder.append("headers=[").append(buildHeadersMap(httpServletRequest)).append("] ");
if (!parameters.isEmpty())
{
stringBuilder.append("parameters=[").append(parameters).append("] ");
}
if (body != null)
{
stringBuilder.append("body=[" + body + "]");
}
ObjectMapper objectMapper = new ObjectMapper();
String jsonInString = null;
try
{
jsonInString = objectMapper.writer().writeValueAsString(body);
}
catch (JsonProcessingException e)
{
throw new RestApiException(HttpStatus.INTERNAL_SERVER_ERROR, e.getMessage());
}
stringBuilder.append("REQUEST Body = [").append(jsonInString).append("] ");
LOGGER.info("BODY DATA >>>> " + jsonInString);
LOGGER.info("Body - : {}" + stringBuilder);
}
private Map<String, String> buildParametersMap(HttpServletRequest httpServletRequest)
{
Map<String, String> resultMap = new HashMap<>();
Enumeration<String> parameterNames = httpServletRequest.getParameterNames();
while (parameterNames.hasMoreElements())
{
String key = parameterNames.nextElement();
String value = httpServletRequest.getParameter(key);
resultMap.put(key, value);
}
return resultMap;
}
private Map<String, String> buildHeadersMap(HttpServletRequest request)
{
Map<String, String> map = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements())
{
String key = headerNames.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
return map;
}
}
I have used ObjectMapper here because I need the body response as raw JSON object, but the afterBodyRead() is invoked after the body is transformed to Java Object.
I found this solved my problem for copying the request buffer for application/json content types. It also shows how to extend the wrapper as the comments to Harshit solution mentions.
https://levelup.gitconnected.com/how-to-log-the-request-body-in-a-spring-boot-application-10083b70c66
The important pieces are that you need a filter to pass along the new request to the server.
#Component
public class LoggingFilter implements Filter {
#Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
if (Arrays.asList("POST", "PUT").contains(httpRequest.getMethod())) {
CustomHttpRequestWrapper requestWrapper = new CustomHttpRequestWrapper(httpRequest);
requestWrapper.setAttribute("input", requestWrapper.getBodyInStringFormat());
filterChain.doFilter(requestWrapper, servletResponse);
return;
}
filterChain.doFilter(servletRequest, servletResponse);
}
}
The logger requires a custom wrapper, and the one spring boot provides seems to be insufficient for application/json type messages.
public class CustomHttpRequestWrapper extends HttpServletRequestWrapper {
public String getBodyInStringFormat() {
return bodyInStringFormat;
}
private final String bodyInStringFormat;
public CustomHttpRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
bodyInStringFormat = readInputStreamInStringFormat(request.getInputStream(), Charset.forName(request.getCharacterEncoding()));
}
private String readInputStreamInStringFormat(InputStream stream, Charset charset) throws IOException {
return getString(stream, charset);
}
static String getString(InputStream stream, Charset charset) throws IOException {
final int MAX_BODY_SIZE = 1024;
final StringBuilder bodyStringBuilder = new StringBuilder();
if (!stream.markSupported()) {
stream = new BufferedInputStream(stream);
}
stream.mark(MAX_BODY_SIZE + 1);
final byte[] entity = new byte[MAX_BODY_SIZE + 1];
final int bytesRead = stream.read(entity);
if (bytesRead != -1) {
bodyStringBuilder.append(new String(entity, 0, Math.min(bytesRead, MAX_BODY_SIZE), charset));
if (bytesRead > MAX_BODY_SIZE) {
bodyStringBuilder.append("...");
}
}
stream.reset();
return bodyStringBuilder.toString();
}
#Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
#Override
public ServletInputStream getInputStream () {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bodyInStringFormat.getBytes());
return new ServletInputStream() {
private boolean finished = false;
#Override
public boolean isFinished() {
return finished;
}
#Override
public int available() {
return byteArrayInputStream.available();
}
#Override
public void close() throws IOException {
super.close();
byteArrayInputStream.close();
}
#Override
public boolean isReady() {
return true;
}
#Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
public int read () {
int data = byteArrayInputStream.read();
if (data == -1) {
finished = true;
}
return data;
}
};
}
}

Read HttpServletRequest payload [duplicate]

I would like to get the XML data from request and response and use it into Rest controller. I tried this:
#RestController()
public class HomeController {
#PostMapping(value = "/v1")
public Response handleMessage(#RequestBody Transaction transaction, HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest request, HttpServletResponse response
System.out.println("!!!!!!! InputStream");
System.out.println(request.getInputStream());
System.out.println(response.getOutputStream());
InputStream in = request.getInputStream();
String readLine;
BufferedReader br = new BufferedReader(new InputStreamReader(in));
while (((readLine = br.readLine()) != null)) {
System.out.println(readLine);
}
}
}
But I get java.io.IOException: UT010029: Stream is closed
What is the proper way to get the content into String variable?
EDIT: I also tried solution with Filter but I'm not aware how to use the request payload into rest controller:
Read request payload:
#Component
public class HttpLoggingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(HttpLoggingFilter.class);
#Override
public void init(FilterConfig filterConfig) throws ServletException {
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
ResettableStreamHttpServletRequest wrappedRequest = new ResettableStreamHttpServletRequest((HttpServletRequest) request);
wrappedRequest.getInputStream().read();
String body = IOUtils.toString(wrappedRequest.getReader());
System.out.println("!!!!!!!!!!!!!!!!!! " + body);
wrappedRequest.resetInputStream();
chain.doFilter(request, response);
}
public class ResettableStreamHttpServletRequest extends HttpServletRequestWrapper {
private byte[] rawData;
private HttpServletRequest request;
private ResettableServletInputStream servletStream;
ResettableStreamHttpServletRequest(HttpServletRequest request) {
super(request);
this.request = request;
this.servletStream = new ResettableServletInputStream();
}
void resetInputStream() {
servletStream.stream = new ByteArrayInputStream(rawData);
}
#Override
public ServletInputStream getInputStream() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getInputStream());
servletStream.stream = new ByteArrayInputStream(rawData);
}
return servletStream;
}
#Override
public BufferedReader getReader() throws IOException {
if (rawData == null) {
rawData = IOUtils.toByteArray(this.request.getInputStream());
servletStream.stream = new ByteArrayInputStream(rawData);
}
String encoding = getCharacterEncoding();
if (encoding != null) {
return new BufferedReader(new InputStreamReader(servletStream, encoding));
} else {
return new BufferedReader(new InputStreamReader(servletStream));
}
}
private class ResettableServletInputStream extends ServletInputStream {
private InputStream stream;
#Override
public int read() throws IOException {
return stream.read();
}
#Override
public boolean isFinished() {
// TODO Auto-generated method stub
return false;
}
#Override
public boolean isReady() {
// TODO Auto-generated method stub
return false;
}
#Override
public void setReadListener(ReadListener readListener) {
// TODO Auto-generated method stub
}
}
}
}
Rest endpoint:
#RestController()
public class HomeController {
#PostMapping(value = "/v1")
public Response handleMessage(#RequestBody Transaction transaction, HttpServletRequest request, org.zalando.logbook.HttpRequest requestv, HttpServletResponse response) throws Exception {
// Get here request and response and log it into DB
}
}
How I can call HttpLoggingFilter into the Java method handleMessage and get the request as body String? Probably I can make it service and Inject it? Can you give me some advice how I can assess the code?
Here are a bunch of classes to do it. This is a once a OncePerRequestFilter implementation, check here https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/filter/OncePerRequestFilter.html. Basically the problem is that in the chain filter, the request stream and response stream can be read just once. So, need to wrap these 2 streams inside something that can be read more than once.
In the first 2 lines I wrapped request and response inside requestToUse and responseToUse. ResettableStreamHttpServletRequest and ResettableStreamHttpServletResponse are wrapper classes that keeps the original string body inside of them, and every time the stream is needed they return a new stream.Then from there, you forget about request and response and start using requestToUse and responseToUse.
I took this from an old project I did. Actually there are more clases, but I extracted the main parts for you. This may not compile right away. Give it a try and let me know and I will help you to make it work.
public class RequestResponseLoggingFilter extends OncePerRequestFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//here you wrap the request and response into some resetable istream class
HttpServletRequest requestToUse = new ResettableStreamHttpServletRequest(request);
HttpServletResponse responseToUse = new ResettableStreamHttpServletResponse(response);
//you read the request to log it
byte[] payload = IOUtils.toByteArray(requestToUse.getReader(), requestToUse.getCharacterEncoding());
String body = new String(payload, requestToUse.getCharacterEncoding());
//here you log the body request
log.(body);
//let the chain continue
filterChain.doFilter(requestToUse, responseToUse);
// Here we log the response
String response = new String(responseToUse.toString().getBytes(), responseToUse.getCharacterEncoding());
//since you can read the stream just once, you will need it again for chain to be able to continue, so you reset it
ResettableStreamHttpServletResponse responseWrapper = WebUtils.getNativeResponse(responseToUse, ResettableStreamHttpServletResponse.class);
if (responseWrapper != null) {
responseWrapper.copyBodyToResponse(true);
}
}
}
public class ResettableStreamHttpServletRequest extends HttpServletRequestWrapper {
private byte[] rawData;
private ResettableServletInputStream servletStream;
public ResettableStreamHttpServletRequest(HttpServletRequest request) throws IOException {
super(request);
rawData = IOUtils.toByteArray(request.getInputStream());
servletStream = new ResettableServletInputStream();
servletStream.setStream(new ByteArrayInputStream(rawData));
}
#Override
public ServletInputStream getInputStream() throws IOException {
servletStream.setStream(new ByteArrayInputStream(rawData));
return servletStream;
}
#Override
public BufferedReader getReader() throws IOException {
servletStream.setStream(new ByteArrayInputStream(rawData));
return new BufferedReader(new InputStreamReader(servletStream));
}
}
public class ResettableStreamHttpServletResponse extends HttpServletResponseWrapper {
private ByteArrayServletOutputStream byteArrayServletOutputStream = new ByteArrayServletOutputStream();
public ResettableStreamHttpServletResponse(HttpServletResponse response) throws IOException {
super(response);
}
/**
* Copy the cached body content to the response.
*
* #param complete whether to set a corresponding content length for the complete cached body content
* #since 4.2
*/
public void copyBodyToResponse(boolean complete) throws IOException {
byte[] array = byteArrayServletOutputStream.toByteArray();
if (array.length > 0) {
HttpServletResponse rawResponse = (HttpServletResponse) getResponse();
if (complete && !rawResponse.isCommitted()) {
rawResponse.setContentLength(array.length);
}
rawResponse.getOutputStream().write(byteArrayServletOutputStream.toByteArray());
if (complete) {
super.flushBuffer();
}
}
}
/**
* The default behavior of this method is to return getOutputStream() on the wrapped response object.
*/
#Override
public ServletOutputStream getOutputStream() throws IOException {
return byteArrayServletOutputStream;
}
/**
* The default behavior of this method is to return getOutputStream() on the wrapped response object.
*/
#Override
public String toString() {
String response = new String(byteArrayServletOutputStream.toByteArray());
return response;
}
}
You dont need to do anything special here, Spring framework will do it for you.
All you need is:
Create a Pojo or Bean which represents your XML data.
Add xml data format dependency to Gradle/Maven which will bind the request xml to your pojo.
compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-xml', version: '2.9.9'
Tell your request handler to accept XML like this:
#RequestMapping(value = "/xmlexample", method = RequestMethod.POST,consumes = "application/xml;charset=UTF-8")
public final boolean transactionHandler(#Valid #RequestBody Transaction transaction) {
log.debug("Received transaction request with data {}", transaction);
return true;
}
And voila, you will have your transaction bean populated with your XML data.

How to log any outgoing responses from #RestController?

I'm using CommonsRequestLoggingFilter to log any incoming requests on my #RestController.
#RestController
public class MyController {
#PostMapping("/")
public MyRsp ping() {
...
return myRsp;
}
}
The users will send POST JSON requests, which are already logged using:
#Bean
public CommonsRequestLoggingFilter requestLoggingFilter() {
CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter();
filter.setIncludeClientInfo(true);
filter.setIncludeQueryString(true);
filter.setIncludePayload(true);
return filter;
}
Question: how can I achieve the same for the JSON Response that I sent back to the user?
I don't know why spring offers a rest logger that uses ContentCachingRequestWrapper, but does not offer response logging. Because it can be implemented quite similar to the req logging, as follows:
public class MyFilter extends CommonsRequestLoggingFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
//same mechanism as for request caching in superclass
HttpServletResponse responseToUse = response;
if (isIncludePayload() && !isAsyncDispatch(request) && !(response instanceof ContentCachingResponseWrapper)) {
responseToUse = new ContentCachingResponseWrapper(response);
}
//outgoing request is logged in superclass
super.doFilterInternal(request, responseToUse, filterChain);
//log incoming response
String rsp = getResponseMessage(responseToUse);
LOGGER.info(rsp);
}
//equivalent to super.createMessage() for request logging
private String getResponseMessage(HttpServletResponse rsp) {
StringBuilder msg = new StringBuilder();
ContentCachingResponseWrapper wrapper =
WebUtils.getNativeResponse(request, ContentCachingResponseWrapper.class);
if (wrapper != null) {
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
int length = Math.min(buf.length, getMaxPayloadLength());
String payload;
try {
payload = new String(buf, 0, length, wrapper.getCharacterEncoding());
}
catch (UnsupportedEncodingException ex) {
payload = "[unknown]";
}
msg.append(";payload=").append(payload);
}
}
}
}
#Pointcut("#annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void requestMapping() {}
#Pointcut("within(path.to your.controller.package.*)")
public void myController() {}
#Around("requestMapping() || myController()")
public MyRsp logAround(ProceedingJoinPoint joinPoint) throws Throwable {
joinPoint.getArgs()// This will give you arguments if any being passed to your controller.
...............
MyRsp myRsp = (MyRsp) joinPoint.proceed();
...............
return myRsp;
}

Categories

Resources