HttpServletRequest getParameter() missing parameter problem

Background

Implemented a basic component can intercept all http requests, the request header/parameter/body/response, etc. for printout. Here the most critical and most difficult to deal with is the output of the body, because the transmission of characters in the body is obtained through the HttpServletRequest byte stream getInputStream (); and this byte stream does not exist after reading once. For example, the use of open source frameworks such as Spring some links have been intercepted to read the input stream, that after our custom interceptor to intercept the print body, the InputStream has no bytes … The implementation of the idea is not difficult, is their own inheritance to achieve HttpServletRequestWrapper, a copy of the body to save a good, and then in the Filter chain transfer can be.

1. knowledge preparation

Content-Type

The current project form transmission is basically the following three types.

application/json request body can be placed in the json string, Spring Controller in the entry using @RequestBody markup. application/x-www-form-urlencoded request body into a=1&b=2&c=3 type queryString key value string structure, Spring Controller in the entry using @RequestParam markup; application/x-www-form-urlencoded. multipart/form-data Used for streaming uploads such as images, such requests do not print the body.

Custom Wrapper

Back up the input stream to a byte array and override getReader() and getInputStream()

 public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper {
     private final byte[] bodyCopier;
 
     public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
         super(request); //Anchor1
         bodyCopier = StreamUtils.copyToByteArray(request.getInputStream());
     }
 
     @Override
     public BufferedReader getReader() throws IOException {
         return new BufferedReader(new InputStreamReader(this.getInputStream()));
     }
 
     @Override
     public ServletInputStream getInputStream() throws IOException{
         return new ServletInputStreamCopier(bodyCopier);
     }
 
     public byte[] getCopy() {
         return this.bodyCopier;
     }
 
     public String getBody() throws UnsupportedEncodingException {
         return new String(this.bodyCopier, GlobalConstants.ENCODE_UTF8);
     }
 }

Custom Byte Streams

 public class ServletInputStreamCopier extends ServletInputStream {
     private ByteArrayInputStream bais;
     
     public ServletInputStreamCopier(byte[] in) {
         this.bais = new ByteArrayInputStream(in);
     }
     
     @Override
     public boolean isFinished() {
         return bais.available() == 0;
     }
     
     @Override
     public boolean isReady() {
         return true;
     }
     
     @Override
     public void setReadListener(ReadListener readListener) {
         throw new RuntimeException("Not implemented");
     }
     
     @Override
     public int read() throws IOException {
         return this.bais.read();
     }
 }

Custom Filter Filter

The original ServletRequest is wrapped in a custom Wrapper through decoration and then transmitted through the filter chain, so that no subsequent reads will cause the data to be empty no matter how many times the body is printed in the interceptor.

 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException,
        ServletException {

    httpRequest = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) servletRequest);
    HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;
    
    chain.doFilter(httpRequest, httpResponse);  
    ...
 }

2. Here’s the problem

problem.png

Test Request.PNG

A web application deployed on Tomcat is using this component and the problem appears to be that the x-www-form-urlencoded request does not enter the controller, which is marked with @RequestParam String username in the method entry of the controller. And the username from request.getParameter(“username”) debugging results are indeed empty, eventually leading to Spring’s Warning log, so can not enter the Controller.

[o.s.w.s.m.s.DefaultHandlerExceptionResolver:189 ] - Handler execution resulted in exception: Required String parameter ‘username’ is not present

Again, I added the request.getParameter(“username”) watchpoint in the IDE, and added a breakpoint in the Anchor1 line of the BodyReaderHttpServletRequestWrapper constructor method, and the watchpoint returns the correct parameter value every time it runs here. parameter value, and the request can also be entered into the controller, the interface request is no problem? The weird thing is that if we remove the Anchor1 breakpoint and let the program run directly, it throws a warning exception that the parameter is not found, and we can’t get into the controller.

At first, I was really baffled by this seemingly “weird” problem, but in fact, if you pay more attention, you will find that the difference between adding a breakpoint and not adding a breakpoint is precisely the monitoring point you add! Add a breakpoint, before running the super() constructor method, first run the request.getParameter() to show that there are values, while not adding a breakpoint, request.getPamameter did not have a chance to run, just wrapped the request and copied a handful of body byte array, and then the parameter empty!

3. problem analysis

Here interested parties can study the specific implementation of request.getParameter(), the source code reference Tomcat RequestFacade, Coyote framework and related implementations, this article does not expand here. After testing, the x-www-form-urlencoded request in Filter first run request.getParameterMap(), then the byte stream of body is cleared; if Filter first run the constructor to copy the body to the byte array, the value of request.getParameterMap() The value of request.getParameterMap() is also not present!

So the conclusion is that in this scenario, request.getParameter() and body are mutually exclusive!

4. Problem solving

The current solution is to run getParameterMap() before constructing the custom Wrapper in Filter to remove the parameter Map from the body, so that any subsequent request.getParamter() will have a value and the Spring controller will run without any problems. so that any subsequent requests.

public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException,
        ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) servletRequest;
        String contentType = httpRequest.getContentType();

        if (contentType ! = null &&
                contentType.contains(HttpContentTypeEnum.APP_X_WWW_FORM_URLENCODE.getContentType())) {
            //If it is application/x-www-form-urlencoded, the parameter value is in the request body as a=1&b=2&c=3... form.
            // If the BodyReaderHttpServletRequestWrapper is constructed directly, after the stream is read and stored in the copy byte array,
            // httpRequest.getParameterMap() will return null!
            // If you run httpRequest.getParameterMap(), the stream in the body will be empty! So the two are mutually exclusive!
            httpRequest.getParameterMap();
        }


        httpRequest = new BodyReaderHttpServletRequestWrapper(httpRequest);
        HttpServletResponse httpResponse = (HttpServletResponse) servletResponse;

5. inscription

Began to refer to Spring’s ContentCachingRequestWrapper to cache the parameter. But one of the basic components do not want to rely heavily on Spring, and the use of this Wrapper will also have the above-mentioned problems, need to customize. In the end, I decided to write it myself.

6. additions

  1. the mark reset function of Inputstream to achieve repeated consumption, in the http scenario, the default is not supported.
  2. the above implementation and servlet-api technology standards, whether there is a conflict, looked through the servlet api specification, did not find getInputStream and getParameter between the description of each other. Coyote underlying specific is first read into the buffer and then encapsulated into the inputStream, whether the two can coexist or not? As a reserved question, leave it for future research.