I want to be able to capture HTML content and then set a header about that content using servlet filter.
I Can capture the content but I always get an error that the response has already been committed.
For example, this is essentially the main wrapper class.
public class BufferedResponseWrapper extends HttpServletResponseWrapper {
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
private PrintWriter writer;
private ServletOutputStream outputStream;
public BufferedResponseWrapper(HttpServletResponse response) {
super(response);
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
if (outputStream == null) {
outputStream = new ServletOutputStream() {
@Override
public boolean isReady() { return true; }
@Override
public void setWriteListener(WriteListener writeListener) {}
@Override
public void write(int b) throws IOException {
buffer.write(b);
}
};
}
return outputStream;
}
@Override
public PrintWriter getWriter() throws IOException {
if (writer == null) {
writer = new PrintWriter(new OutputStreamWriter(buffer, getCharacterEncoding()), true);
}
return writer;
}
@Override
public void flushBuffer() throws IOException {
// DO NOT commit the real response here
if (writer != null) writer.flush();
if (outputStream != null) outputStream.flush();
}
public byte[] getCapturedContent() throws IOException {
flushBuffer();
return buffer.toByteArray();
}
}
...
And use of the filter:
public class CspFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletResponse original = (HttpServletResponse) res;
BufferedResponseWrapper wrapped = new BufferedResponseWrapper(original);
// Capture everything downstream
chain.doFilter(req, wrapped);
// Get full HTML content
byte[] captured = wrapped.getCapturedContent();
String html = new String(captured, original.getCharacterEncoding());
// Compute hashes
String algoData = computeAlgo(html); // your logic
// Set headers BEFORE writing body (this fails with error
original.setHeader("X-Company-Data-About-Html", algoData);
original.setContentLength(captured.length);
// Write out the final buffered response
ServletOutputStream out = original.getOutputStream();
out.write(captured);
out.flush();
}
}
...
The error message is, will this work or is there approach?
"[WARNING ] SRVE8094W: Warning cannot set header. Response already committed."
At this line: original.setHeader("X-Company-Data-About-Html", algoData);
This legacy code for Java 8, Websphere Application Server v8 if that is relevant here. And the J2EE spec for that configuration v3.
The code comment here,
@Override
public void flushBuffer() throws IOException {
// DO NOT commit the real response here
if (writer != null) writer.flush();
if (outputStream != null) outputStream.flush();
}
indicates a misunderstanding of what a "commit" is. It is not the situation which happens when you close the writer/stream. It will already happen when you flush the writer/stream, which you're thus doing in the very same method. Basically, the flush will actually send the so far written bytes from the server to the client, after sending the headers. It's at that moment thus too late to modify the already-sent headers. This is the "committed" state.
You need to postpone the flush until the very last byte is written to the writer/stream. Only then you'll be able to modify the headers. After which you perform the actual flush (and close). You can find here an example of a HttpServletResponseWrapper
which correctly does that: Catch-all servlet filter that should capture ALL HTML input content for manipulation, works only intermittently.