3

Currently im developing a REST API using RestEasy and Jetty. One of my plan with this REST API is to create a hook plugin to do anything needed with the incoming request utilizing JAX-RS ContainerRequestFilter . The thing with ContainerRequestPlugin in Jetty here is that once I called requestContext.getEntityStream(); in the Filter then the request wont be able to be read again by my EndPoint Class even if I have set the Entity Stream again.

Following are my Filter code

@Provider
@Priority(2000)
public class DummyRequestFilter implements ContainerRequestFilter{
    static Logger log = Logger.getLogger(DummyRequestFilter .class.getName());
    
    @Context
    private HttpServletRequest servletRequest;
    
    @Override
    public void filter(ContainerRequestContext requestContext) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
    String requestBody = "";
    
    try {           
        IOUtils.copy(requestContext.getEntityStream(), baos);
        
        InputStream is1 = new ByteArrayInputStream(baos.toByteArray());
        InputStream is2 = new ByteArrayInputStream(baos.toByteArray());
        
        requestBody = IOUtils.toString(is1);
        
        log.info(requestBody);
        
        requestContext.setEntityStream(is2);
                
    }catch (Exception e) {
        log.log(Level.SEVERE,"Exception Occurred",e);
    }
    }   
}

Then here is my endpoint class

@Path("/")
public class DummyService {
    
    Logger log = Logger.getLogger(DummyService .class.getName());
    
    @GET
    @Path("test")
    @Produces(MediaType.APPLICATION_JSON)
    public Response test(@FormParam("name") String name) {
        log.info("Name = "+name);

        return Response.status(200).build();
    }
}

Whenever I called this test method I can see the name sent in Filter class but in the Endpoint class name is NULL.

Later then I figured out that the getEntityStream returned from requestContext is Jetty custom ServletInputStream that is org.eclipse.jetty.server.HttpInput. I believe the request cannot be read in EndPoint since I set the Entity Stream using ByteArrayInputStream.

So my question will be, is there any way to build/convert Jetty HttpInput using generic InputStream implementation? or is there any other way to work around this case? where I can read Jetty HttpInput many times?

Thanks & Regards

yjatip
  • 37
  • 2
  • 1
    You may find some help [here](https://stackoverflow.com/questions/9501237/read-stream-twice) - you might need to save the input stream off somewhere and re-add it once done reading, since they are designed to be read once. – Gryphon Aug 12 '20 at 14:00

1 Answers1

1

As you have no doubt noticed, the Servlet spec does not allow you to read the Request body contents twice.

This is an intentional decision as any such feature would require caching or buffering the response body content. Which leads to:

  • Various DoS / Denial of Service attacks against your webapp.
  • Idle Timeouts on request processing when your code reads the request the second time from the buffer and produces no network traffic to reset the idle timeout.
  • The inability to benefit from or use Servlet Async I/O processing.

JAX-RS endpoints typically require that the javax.servlet.http.HttpServletRequest input stream has not been read, at all, for any reason (*).

Your code makes no attempt to limit the size of the byte arrays you allocate, it would be easy to abuse your service with a Zip Bomb. (example: sending 42 kilobytes of data that unpacks to 3.99 petabytes)

You may find a JAX-RS implementation specific way, such as using Jersey internal code to set the entity stream, but that kind of code will be fragile and likely result in the need to fix your code and recompile with updates to your Jersey library.

If you go the custom route, please be take extra care to not introduce obvious vulnerabilities in your code, limit your request size, limit what you can buffer, etc.

Typically webapps that need to modify request input stream content do it via proxy servlets that perform middle-man modification of the request in real-time, on a buffer by buffer basis. Jetty has such a class, called conveniently AsyncMiddleManServlet. This essentially means your client talks to the proxy which talks to your endpoint, which honors network behaviors and network backpressure needs. (something a buffering filter wouldn't be able to handle properly)

(*) You can accidentally read the HttpServletRequest body by using things from the request that ask for the request parameters or the request parts (which require that the body content be read for certain specific Content-Types)

Joakim Erdfelt
  • 46,896
  • 7
  • 86
  • 136
  • Wow, thanks a lot Sir for the detailed explanation!, really appreciate your input. I just realized that this is intentionally done for security reason. Actually I need to log all the request and response into DB and probably in the future I will need to somehow modify the request/response body. By the way is there any best practice to log request and response (insert into DB) using JAX-RS and Jetty other than using ContainerRequestFilter? again thanks – yjatip Aug 26 '20 at 17:12
  • Implement the Jetty Interceptors (input and output) to capture the request body or response body, you can even insert them before/after things like gzip compression to see what the body looks like without compression. That way you can capture the raw stream of bytes, and the servlet API isn't even involved (or knows about the existence of the interceptors) – Joakim Erdfelt Aug 26 '20 at 18:27
  • Thanks a lot, you really save me Sir! ;) – yjatip Aug 26 '20 at 19:34