How to Read and Copy the Http Servlet Response Output Stream Content for Logging

How to read and copy the HTTP servlet response output stream content for logging

You need to create a Filter wherein you wrap the ServletResponse argument with a custom HttpServletResponseWrapper implementation wherein you override the getOutputStream() and getWriter() to return a custom ServletOutputStream implementation wherein you copy the written byte(s) in the base abstract OutputStream#write(int b) method. Then, you pass the wrapped custom HttpServletResponseWrapper to the FilterChain#doFilter() call instead and finally you should be able to get the copied response after the the call.

In other words, the Filter:

@WebFilter("/*")
public class ResponseLogger implements Filter {

@Override
public void init(FilterConfig config) throws ServletException {
// NOOP.
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
if (response.getCharacterEncoding() == null) {
response.setCharacterEncoding("UTF-8"); // Or whatever default. UTF-8 is good for World Domination.
}

HttpServletResponseCopier responseCopier = new HttpServletResponseCopier((HttpServletResponse) response);

try {
chain.doFilter(request, responseCopier);
responseCopier.flushBuffer();
} finally {
byte[] copy = responseCopier.getCopy();
System.out.println(new String(copy, response.getCharacterEncoding())); // Do your logging job here. This is just a basic example.
}
}

@Override
public void destroy() {
// NOOP.
}

}

The custom HttpServletResponseWrapper:

public class HttpServletResponseCopier extends HttpServletResponseWrapper {

private ServletOutputStream outputStream;
private PrintWriter writer;
private ServletOutputStreamCopier copier;

public HttpServletResponseCopier(HttpServletResponse response) throws IOException {
super(response);
}

@Override
public ServletOutputStream getOutputStream() throws IOException {
if (writer != null) {
throw new IllegalStateException("getWriter() has already been called on this response.");
}

if (outputStream == null) {
outputStream = getResponse().getOutputStream();
copier = new ServletOutputStreamCopier(outputStream);
}

return copier;
}

@Override
public PrintWriter getWriter() throws IOException {
if (outputStream != null) {
throw new IllegalStateException("getOutputStream() has already been called on this response.");
}

if (writer == null) {
copier = new ServletOutputStreamCopier(getResponse().getOutputStream());
writer = new PrintWriter(new OutputStreamWriter(copier, getResponse().getCharacterEncoding()), true);
}

return writer;
}

@Override
public void flushBuffer() throws IOException {
if (writer != null) {
writer.flush();
} else if (outputStream != null) {
copier.flush();
}
}

public byte[] getCopy() {
if (copier != null) {
return copier.getCopy();
} else {
return new byte[0];
}
}

}

The custom ServletOutputStream:

public class ServletOutputStreamCopier extends ServletOutputStream {

private OutputStream outputStream;
private ByteArrayOutputStream copy;

public ServletOutputStreamCopier(OutputStream outputStream) {
this.outputStream = outputStream;
this.copy = new ByteArrayOutputStream(1024);
}

@Override
public void write(int b) throws IOException {
outputStream.write(b);
copy.write(b);
}

public byte[] getCopy() {
return copy.toByteArray();
}

}

How can I read an HttpServletReponses output stream?

Add this to the filter java file.

static class MyHttpServletResponseWrapper 
extends HttpServletResponseWrapper {

private StringWriter sw = new StringWriter(BUFFER_SIZE);

public MyHttpServletResponseWrapper(HttpServletResponse response) {
super(response);
}

public PrintWriter getWriter() throws IOException {
return new PrintWriter(sw);
}

public ServletOutputStream getOutputStream() throws IOException {
throw new UnsupportedOperationException();
}

public String toString() {
return sw.toString();
}
}

Use the follow code:

HttpServletResponse httpResponse = (HttpServletResponse) response;
MyHttpServletResponseWrapper wrapper =
new MyHttpServletResponseWrapper(httpResponse);

chain.doFilter(request, wrapper);

String content = wrapper.toString();

The content variable now has the output stream. You can also do it for binary content.

Capture and log the response body

If I understand you correctly, you want to log the response body? That's a pretty expensive task, but if that's the business requirement...

As @duffymo pointed, a Filter is a suitable place for this. You can capture the response body by replacing the passed-in ServletResponse with a HttpServletResponseWrapper implementation which replaces the HttpServletResponse#getWriter() with an own implementation which copies the response body into some buffer. After continuing the filter chain with the replaced response, just log the copy.

Here's a kickoff example how the doFilter() method can look like:

public void doFilter(ServletRequest request, final ServletResponse response, FilterChain chain) throws IOException, ServletException {
final CopyPrintWriter writer = new CopyPrintWriter(response.getWriter());
chain.doFilter(request, new HttpServletResponseWrapper((HttpServletResponse) response) {
@Override public PrintWriter getWriter() {
return writer;
}
});
logger.log(writer.getCopy());
}

Here's how the CopyPrintWriter can look like:

public class CopyPrintWriter extends PrintWriter {

private StringBuilder copy = new StringBuilder();

public CopyPrintWriter(Writer writer) {
super(writer);
}

@Override
public void write(int c) {
copy.append((char) c); // It is actually a char, not an int.
super.write(c);
}

@Override
public void write(char[] chars, int offset, int length) {
copy.append(chars, offset, length);
super.write(chars, offset, length);
}

@Override
public void write(String string, int offset, int length) {
copy.append(string, offset, length);
super.write(string, offset, length);
}

public String getCopy() {
return copy.toString();
}

}

Map this filter on an url-pattern for which you'd like to log responses for. Keep in mind that binary/static content like images, CSS, JS files and so on won't be logged this way. You'd like to exclude them by using a specific enough url-pattern, e.g. *.jsp or just on the servlet-name of the servlet in question. If you want to log binary/static content anyway (for which I don't see any benefit), then you need to replace the HttpServletResponse#getOutputStream() the same way as well.

How to read servlet response output stream in Node JS?

You can pipe the response stream back like below,

router.get('/openfile', function(req, res, next) {
req.pipe(request.get('http://localhost:8080/getFileContent')).pipe(res); });

Spring MVC - Get HttpServletResponse body

It's been hard on searching deeply into it but found that ResponseBodyAdvice could be suitable for my purposes. So looking for some example on StackOverflow found this guy which had quite same issue having to manipulate the Object body.

That's my final working solution in order to implement what I wrote here

@ControllerAdvice
public class CSRFHandler implements ResponseBodyAdvice<Object> {

@Value("${security.csrf.enabled}")
private String csrfEnabled;

@Value("${security.csrf.headerName}")
private String csrfHeaderName;

@Value("${security.csrf.salt}")
private String salt;

@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 (new Boolean(csrfEnabled).booleanValue()) {
String csrfValue = SecureUtil.buildCsrfValue(salt, StringUtil.toJson(body));
response.getHeaders().add(csrfHeaderName, csrfValue);
}

return body;
}

}

Logging response body (HTML) from HttpServletResponse using Spring MVC HandlerInterceptorAdapter

This would be better done using a Servlet Filter rather than a Spring HandlerInterceptor, for the reason that a Filter is allowed to substitute the request and/or response objects, and you could use this mechanism to substitute the response with a wrapper which logs the response output.

This would involve writing a subclass of HttpServletResponseWrapper, overriding getOutputStream (and possibly also getWriter()). These methods would return OutputStream/PrintWriter implementations that siphon off the response stream into a log, in addition to sending to its original destination. An easy way to do this is using TeeOutputStream from Apache Commons IO, but it's not hard to implement yourself.

Here's an example of the sort of thing you could do, making use of Spring's GenericFilterBean and DelegatingServletResponseStream, as well as TeeOutputStream, to make things easier:

public class ResponseLoggingFilter extends GenericFilterBean {

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
HttpServletResponse responseWrapper = loggingResponseWrapper((HttpServletResponse) response);
filterChain.doFilter(request, responseWrapper);
}

private HttpServletResponse loggingResponseWrapper(HttpServletResponse response) {
return new HttpServletResponseWrapper(response) {
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new DelegatingServletOutputStream(
new TeeOutputStream(super.getOutputStream(), loggingOutputStream())
);
}
};
}

private OutputStream loggingOutputStream() {
return System.out;
}
}

This logs everything to STDOUT. If you want to log to a file, it'll get a big more complex, what with making sure the streams get closed and so on, but the principle remains the same.



Related Topics



Leave a reply



Submit