10

I have a need where certain HTTP requests must be redirected to a Spring Boot web app/service, but that on the request-side, the Spring app does nothing and acts as a passthrough between the HTTP client (another service) and the request's true destination. But when the response comes back to the Spring app (from that destination), I need the Spring app to be able to inspect the response and possibly take action on it if need be. So:

  1. HTTP client makes a request to, say, http://someapi.example.com
  2. Network magic routes the request to my Spring app at, say, http://myproxy.example.com
  3. On the request, this app/proxy does nothing, and so the request is forwarded on http://someapi.example.com
  4. The service endpoint at http://someapi.example.com returns an HTTP response back to the proxy
  5. The proxy at http://myproxy.example.com inspects this response, and possibly sends an alert before returning the response back to the original client

So essentially, a filter that acts as a pass-through on the request, and only really does anything after the remote service has executed and returned a response.

My best attempt thus far has been to setup a servlet filter:

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

    // How and where do I put my code?
    if(responseContainsFizz(response)) {
        // Send an alert (don't worry about this code)
    }
}

Is this possible to do? If so, where do I put the code that inspects and acts upon the response? With my code the way it is I get exceptions thrown when trying to hit a controller from a browser:

java.lang.IllegalStateException: STREAM
    at org.eclipse.jetty.server.Response.getWriter(Response.java:910) ~[jetty-server-9.2.16.v20160414.jar:9.2.16.v20160414]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_92]
  rest of stack trace omitted for brevity

Any ideas?

smeeb
  • 27,777
  • 57
  • 250
  • 447
  • 1
    Spring AOP Aspect/Pointcut can do this (example: http://stackoverflow.com/questions/3310115/spring-aop-advice-on-annotated-controllers) – MikeM Jan 18 '17 at 10:43
  • Thanks @MikeM (+1) - For the record, I'm not *opposed* to an AOP approach, but **only** if it was truly *not possible* to do this with filters! Thanks again! – smeeb Jan 18 '17 at 10:55
  • Another idea that I would try is to write custom interceptor and use its `afterActionCompletion()` method. See: http://docs.spring.io/spring/docs/4.3.x/javadoc-api/org/springframework/web/portlet/HandlerInterceptor.html#afterActionCompletion-javax.portlet.ActionRequest-javax.portlet.ActionResponse-java.lang.Object-java.lang.Exception- – Slava Semushin Jan 18 '17 at 11:23
  • have you established the fact that the filter alone works? i.e without trying to access the response from the destination. – gipsy Jan 20 '17 at 14:55
  • Yes, I can verify that I've setup the filter correctly and that it if I add an print/log statements prior to `chain.doFilter(...)`, they are executed as expected. – smeeb Jan 20 '17 at 14:56
  • So potentially something in responseContainsFizz is causing this. can we see the code for that method? – gipsy Jan 20 '17 at 15:00
  • Oh ! You meant to say if you have chain.doFilter(request, response) in place it fails? – gipsy Jan 20 '17 at 15:01
  • Right exactly, `responseContainsFizz` is definitely **not** to blame here, problem is with the doFilter. – smeeb Jan 20 '17 at 15:13
  • Check my answer. The point 1 to 5 in your question described a typical filter case, despite of the usage of "remote endpoint", probably because of the "Network magic route". If my answer helps, accept and upvote it. If not, provide more info. – code_angel Jan 26 '17 at 09:51

2 Answers2

5

Per the Servlet API documentation, the reason you are getting the IllegalStateException is because you are attempting to call ServletResponse.getWriter after ServletResponse.getOutputStream has already been called on the response. So it appears that the method you need to call is ServletResponse.getOutputStream().

However, if you are trying to access the body of the response, the best solution is to wrap the response in a ServletResponseWrapper so that you can capture the data:

public class MyFilter implements Filter
{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException
    {

    }

    @Override
    public void destroy()
    {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
    {
        MyServletResponseWrapper responseWrapper = new MyServletResponseWrapper((HttpServletResponse) response);
        chain.doFilter(request, responseWrapper);
        if (evaluateResponse(responseWrapper)) {
            // Send an alert
        }
    }

    private boolean evaluateResponse(MyServletResponseWrapper responseWrapper) throws IOException
    {
        String body = responseWrapper.getResponseBodyAsText();

        // Perform business logic on the body text

        return true;
    }

    private static class MyServletResponseWrapper extends HttpServletResponseWrapper
    {
        private ByteArrayOutputStream copyOutputStream;
        private ServletOutputStream wrappedOutputStream;

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

        public String getResponseBodyAsText() throws IOException
        {
            String encoding = getResponse().getCharacterEncoding();
            return copyOutputStream.toString(encoding);
        }


        @Override
        public ServletOutputStream getOutputStream() throws IOException
        {
            if (wrappedOutputStream == null) {
                wrappedOutputStream = getResponse().getOutputStream();
                copyOutputStream = new ByteArrayOutputStream();
            }
            return new ServletOutputStream()
            {
                @Override
                public boolean isReady()
                {
                    return wrappedOutputStream.isReady();
                }

                @Override
                public void setWriteListener(WriteListener listener)
                {
                    wrappedOutputStream.setWriteListener(listener);
                }

                @Override
                public void write(int b) throws IOException
                {
                    wrappedOutputStream.write(b);
                    copyOutputStream.write(b);
                }

                @Override
                public void close() throws IOException
                {
                    wrappedOutputStream.close();
                    copyOutputStream.close();
                }
            };
        }
    }
}
2

The response can be easy manipulated/replaced/extended e with a filter and a response wrapper.

In the filter before the call chain.doFilter(request, wrapper) you prepare a PrintWriter for the new response content and the wrapper object.

After the call chain.doFilter(request, wrapper) is the actuall response manipulation.

The wrapper is used to get access to the response as String.

The Filter:

@WebFilter(filterName = "ResponseAnalysisFilter", urlPatterns = { "/ResponseFilterTest/*" })
public class ResponseFilter implements Filter {
    public ResponseFilter() {}

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {}

    @Override
    public void destroy() {}

    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
            throws IOException, ServletException {
        PrintWriter out = response.getWriter();
        CharResponseWrapper wrapper = new CharResponseWrapper((HttpServletResponse) response);
        chain.doFilter(request, wrapper);

        String oldResponseString = wrapper.toString();

        if (oldResponseString.contains("Fizz")) { 
            // replace something
            String newResponseString = oldResponseString.replaceAll("Fizz", "Cheers");
            // show alert with a javascript appended in the head tag
            newResponseString = newResponseString.replace("</head>", 
               "<script>alert('Found Fizz, replaced with Cheers');</script></head>");

            out.write(newResponseString);
            response.setContentLength(newResponseString.length());
        } 
        else { //not changed
            out.write(oldResponseString);
        }
        // the above if-else block could be replaced with the code you need.
        // for example: sending notification, writing log, etc.

        out.close();
    }
}

The Response Wrapper:

public class CharResponseWrapper extends HttpServletResponseWrapper {
    private CharArrayWriter output;

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

    public CharResponseWrapper(HttpServletResponse response) {
        super(response);
        output = new CharArrayWriter();
    }

    public PrintWriter getWriter() {
        return new PrintWriter(output);
    }
}

The Test Servlet:

@WebServlet("/ResponseFilterTest/*")
public class ResponseFilterTest extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        response.setContentType("text/html");
        response.setCharacterEncoding("utf-8");

        response.getWriter().append(
           "<html><head><title>replaceResponse filter</title></head><body>");

        if (request.getRequestURI().contains("Fizz")) {
            response.getWriter().append("Fizz");
        }
        else {
            response.getWriter().append("Limo");
        }

        response.getWriter().append("</body></html>");
    }
}

Test Urls:

More Info and examples about filters:
http://www.oracle.com/technetwork/java/filters-137243.html#72674
http://www.leveluplunch.com/java/tutorials/034-modify-html-response-using-filter/
https://punekaramit.wordpress.com/2010/03/16/intercepting-http-response-using-servlet-filter/

code_angel
  • 1,537
  • 1
  • 11
  • 21