Looking for an Example for Inserting Content into the Response Using a Servlet Filter

Looking for an example for inserting content into the response using a servlet filter

The codebase I am using, calls the getOutputStream method, instead of getWriter when it processes the response, so the examples included in the other answer doesn't help. Here is a more complete answer that works with both the OutputStream and the PrintWriter, even erroring correctly, if the writer is accessed twice. This is derived from the great example, DUMP REQUEST AND RESPONSE USING JAVAX.SERVLET.FILTER.

import javax.servlet.*;
import javax.servlet.http.*;
import java.io.*;

public class MyFilter implements Filter
{
private FilterConfig filterConfig = null;

private static class ByteArrayServletStream extends ServletOutputStream
{
ByteArrayOutputStream baos;

ByteArrayServletStream(ByteArrayOutputStream baos)
{
this.baos = baos;
}

public void write(int param) throws IOException
{
baos.write(param);
}
}

private static class ByteArrayPrintWriter
{

private ByteArrayOutputStream baos = new ByteArrayOutputStream();

private PrintWriter pw = new PrintWriter(baos);

private ServletOutputStream sos = new ByteArrayServletStream(baos);

public PrintWriter getWriter()
{
return pw;
}

public ServletOutputStream getStream()
{
return sos;
}

byte[] toByteArray()
{
return baos.toByteArray();
}
}

public class CharResponseWrapper extends HttpServletResponseWrapper
{
private ByteArrayPrintWriter output;
private boolean usingWriter;

public CharResponseWrapper(HttpServletResponse response)
{
super(response);
usingWriter = false;
output = new ByteArrayPrintWriter();
}

public byte[] getByteArray()
{
return output.toByteArray();
}

@Override
public ServletOutputStream getOutputStream() throws IOException
{
// will error out, if in use
if (usingWriter) {
super.getOutputStream();
}
usingWriter = true;
return output.getStream();
}

@Override
public PrintWriter getWriter() throws IOException
{
// will error out, if in use
if (usingWriter) {
super.getWriter();
}
usingWriter = true;
return output.getWriter();
}

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

public void init(FilterConfig filterConfig) throws ServletException
{
this.filterConfig = filterConfig;
}

public void destroy()
{
filterConfig = null;
}

public void doFilter(
ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
CharResponseWrapper wrappedResponse = new CharResponseWrapper(
(HttpServletResponse)response);

chain.doFilter(request, wrappedResponse);
byte[] bytes = wrappedResponse.getByteArray();

if (wrappedResponse.getContentType().contains("text/html")) {
String out = new String(bytes);
// DO YOUR REPLACEMENTS HERE
out = out.replace("</head>", "WTF</head>");
response.getOutputStream().write(out.getBytes());
}
else {
response.getOutputStream().write(bytes);
}
}
}

Replace existing response with new using servlet filter in spring mvc

I feel below code snippet should work.The problem with above is it is getting appended to the existing data instead of rewriting. We need to create a copy with where the data is stored and copy after manipulating to the original ServletResponse. Hope below code snippet solve your issue.

below is the main do filter method-

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

ServletResponseWrapperCopier capturingResponseWrapper = new ServletResponseWrapperCopier(
(HttpServletResponse) response);

chain.doFilter(request, capturingResponseWrapper);
try{
String respopn = capturingResponseWrapper.getCaptureAsString();
JSONObject json=new JSONObject(respopn);
JSONObject dMap=new JSONObject(json.get("dataMap"));
dMap.put("new", "newValue");
json.put("dataMap", dMap); // Modified the old datamap with new json
JsonMapper jsonMap=new JsonMapper();
jsonMap.setJson(json);
String str=jsonMap.getJson();
response.getOutputStream().write(str.getBytes());
} catch(Exception e){
log.error("");
}
}

Below class is used to copy the ServletResponse in the above code snippet

public class ServletResponseWrapperCopier extends HttpServletResponseWrapper{

private final ByteArrayOutputStream capture;
private ServletOutputStream output;
private PrintWriter writer;

public ServletResponseWrapperCopier(HttpServletResponse response) {
super(response);
capture = new ByteArrayOutputStream(response.getBufferSize());
}

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

if (output == null) {
output = new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
capture.write(b);
}

@Override
public void flush() throws IOException {
capture.flush();
}

@Override
public void close() throws IOException {
capture.close();
}
};
}

return output;
}

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

if (writer == null) {
writer = new PrintWriter(new OutputStreamWriter(capture,
getCharacterEncoding()));
}

return writer;
}

public byte[] getCaptureAsBytes() throws IOException {
if (writer != null) {
writer.close();
} else if (output != null) {
output.close();
}

return capture.toByteArray();
}

public String getCaptureAsString() throws IOException {
return new String(getCaptureAsBytes(), getCharacterEncoding());
}

}

Adding header in response in filter?

After filterChain.doFilter is called it's too late to do anything with the response. At this point, the entire response was already sent to the client.

You need to build a wrap response into your own classes, pass these wrappers into doFilter method and handle any processing in your wrappers.

There is already a response wrapper: HttpServletResponseWrapper that you can extend. For example:

public class MyResponseRequestWrapper extends HttpServletResponseWrapper{
public MyResponseRequestWrapper(HttpServletResponse response) {
super(response);
}
}

Your filter:

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

HttpServletResponse myResponse = (HttpServletResponse) response;
MyResponseRequestWrapper responseWrapper = new MyResponseRequestWrapper(myResponse);
responseWrapper.addHeader("Access-Control-Allow-Origin", "*");
filterChain.doFilter(request, myResponse);
}

Java Servlet filter: I have to add headers before passing to the chain, documentation tells otherwise

I do not know what version of the Servlet spec are you referring to, but in 3.1, chapter 6.2.1 "Filter Lifecycle" I read (emphasis mine):


  1. After invocation of the next filter in the chain, the filter may examine response headers.

"Examine" not "set"! Actually, the spec for the header methods says (Servlet 3.1, chapter 5.2 "Headers"):

To be successfully transmitted back to the client, headers must be set before the response is committed. Headers set after the response is committed will be ignored by the servlet container.

I guess this is happening to your request, some servlet or filter down the chain is "committing", so headers are ignored.

Bottom line: The spec (at least as far as I can see in 3.1) does not suggest to set the headers after calling chain.doFilter(). Your second version that works is correct (and how I've always implemented filters that add headers)!

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();
}

}

Catch-all servlet filter that should capture ALL HTML input content for manipulation, works only intermittently

Your initial approach failed because PrintWriter wraps the given ByteArrayOutputStream with a BufferedWriter which has an internal character buffer of 8192 characters, and you never flush() the buffer before getting the bytes from the ByteArrayOutputStream. In other words, when less than ~8KB of data is written to the getWriter() of the response, the wrapped ByteArrayOutputStream actually never get filled; namely everything is still in that internal character buffer, waiting to be flushed.

A fix would be to perform a flush() call before toByteArray() in your MyPrintWriter:

byte[] toByteArray() {
pw.flush();
return baos.toByteArray();
}

This way the internal character buffer will be flushed (i.e. it will actually write everything to the wrapped stream). This also totally explains why it works when you write to getOutputStream(), this step namely doesn't use the PrintWriter and nothing gets buffered in some internal buffer.


Unrelated to the concrete problem: this approach has some severe problems. It isn't respecting the response character encoding during construction of PrintWriter (you should actually wrap the ByteArrayOutputStream in an OutputStreamWriter instead which can take a character encoding) and relying on the platform default, in other words, any written Unicode characters may end up in Mojibake this way and thus this approach isn't ready for World Domination.

Also, this approach makes it possible to call both getWriter() and getOutputStream() on the same response, while that's considered an illegal state (precisely to avoid this kind of buffering and encoding trouble).


Update as per the comment, here's a full rewrite of the response wrapper, showing the right way, hopefully in a more self-explaining way than the code you've so far:

public class CapturingResponseWrapper extends HttpServletResponseWrapper {

private final ByteArrayOutputStream capture;
private ServletOutputStream output;
private PrintWriter writer;

public CapturingResponseWrapper(HttpServletResponse response) {
super(response);
capture = new ByteArrayOutputStream(response.getBufferSize());
}

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

if (output == null) {
output = new ServletOutputStream() {
@Override
public void write(int b) throws IOException {
capture.write(b);
}
@Override
public void flush() throws IOException {
capture.flush();
}
@Override
public void close() throws IOException {
capture.close();
}
};
}

return output;
}

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

if (writer == null) {
writer = new PrintWriter(new OutputStreamWriter(capture, getCharacterEncoding()));
}

return writer;
}

@Override
public void flushBuffer() throws IOException {
super.flushBuffer();

if (writer != null) {
writer.flush();
}
else if (output != null) {
output.flush();
}
}

public byte[] getCaptureAsBytes() throws IOException {
if (writer != null) {
writer.close();
}
else if (output != null) {
output.close();
}

return capture.toByteArray();
}

public String getCaptureAsString() throws IOException {
return new String(getCaptureAsBytes(), getCharacterEncoding());
}

}

Here's how you're supposed to use it:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
CapturingResponseWrapper capturingResponseWrapper = new CapturingResponseWrapper((HttpServletResponse) response);
chain.doFilter(request, capturingResponseWrapper);
String content = capturingResponseWrapper.getCaptureAsString(); // This uses response character encoding.
String replacedContent = content.replaceAll("(?i)</form(\\s)*>", "<input type=\"hidden\" name=\"zval\" value=\"fromSiteZ123\"/></form>");
response.getWriter().write(replacedContent); // Don't ever use String#getBytes() without specifying character encoding!
}


Related Topics



Leave a reply



Submit