0

(Java ver. 8)

I need to process the request body in a filter. Using the below code, I read the body.

    private static String convertInputStreamToString(InputStream is) throws IOException {

        ByteArrayOutputStream result = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024 * 50];
        int length;
        while ((length = is.read(buffer)) != -1) {
            result.write(buffer, 0, length);
        }

        return result.toString("UTF-8");
    }

The issue is if there are parameters posted by request body with the content type "application/x-www-form-urlencoded", then the parameters won't be available after reading the body. They are available to get using request.getParameter(), if I don't read the body.

Moreover, I tried using the below code to wrap the request and provide the body, so it would be available to the rest of the solution (e.g. servlets), but the issue with losing the parameters happens yet. code is copied/adopted from this post

public class RequestWrapper extends HttpServletRequestWrapper {

    private final String body;

    public RequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        
        body = convertInputStreamToString(request.getInputStream());
    }

    private static String convertInputStreamToString(InputStream is) throws IOException {

        ByteArrayOutputStream result = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024 * 50];
        int length;
        while ((length = is.read(buffer)) != -1) {
            result.write(buffer, 0, length);
        }

        return result.toString("UTF-8");
    }
    
    @Override
    public ServletInputStream getInputStream() throws IOException {
        final byte[] myBytes = body.getBytes("UTF-8");
        ServletInputStream servletInputStream = new ServletInputStream() {
            private int lastIndexRetrieved = -1;
            private ReadListener readListener = null;

            @Override
            public boolean isFinished() {
                return (lastIndexRetrieved == myBytes.length - 1);
            }

            @Override
            public boolean isReady() {
                return isFinished();
            }

            @Override
            public void setReadListener(ReadListener readListener) {
                this.readListener = readListener;
                if (!isFinished()) {
                    try {
                        readListener.onDataAvailable();
                    } catch (IOException e) {
                        readListener.onError(e);
                    }
                } else {
                    try {
                        readListener.onAllDataRead();
                    } catch (IOException e) {
                        readListener.onError(e);
                    }
                }
            }

            @Override
            public int read() throws IOException {
                int i;
                if (!isFinished()) {
                    i = myBytes[lastIndexRetrieved + 1];
                    lastIndexRetrieved++;
                    if (isFinished() && (readListener != null)) {
                        try {
                            readListener.onAllDataRead();
                        } catch (IOException ex) {
                            readListener.onError(ex);
                            throw ex;
                        }
                    }
                    return i;
                } else {
                    return -1;
                }
            }
        };
        return servletInputStream;
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }
}
zaerymoghaddam
  • 3,037
  • 1
  • 27
  • 33
Reza A
  • 1
  • 2

3 Answers3

0

I tried to run the code you mentioned you're using and I think the accepted answer may not solve your issue as it's quite old. Seems you also need to overwrite the getParameter, getParameterMap and getParameterValues methods. I tried to do that based on this answer from the same post and seems it works. Here is the code:

public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
    private ByteArrayOutputStream cachedBytes;
    private String body;
    private Map<String, String[]> parameterMap;

    public MultiReadHttpServletRequest(HttpServletRequest request) throws IOException {
        super(request);
        parameterMap = super.getParameterMap();
        cacheBodyAsString();
        System.out.println("The Body read into a String is: " + body);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (cachedBytes == null)
            cacheInputStream();

        return new CachedServletInputStream(cachedBytes.toByteArray());
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    @Override
    public String getParameter(String key) {
        Map<String, String[]> parameterMap = getParameterMap();
        String[] values = parameterMap.get(key);
        return values != null && values.length > 0 ? values[0] : null;
    }

    @Override
    public String[] getParameterValues(String key) {
        Map<String, String[]> parameterMap = getParameterMap();
        return parameterMap.get(key);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return parameterMap;
    }

    private void cacheInputStream() throws IOException {
        // Cache the inputstream in order to read it multiple times
        cachedBytes = new ByteArrayOutputStream();

        byte[] buffer = new byte[1024 * 50];
        int length;
        InputStream is = super.getInputStream();
        while ((length = is.read(buffer)) != -1) {
            cachedBytes.write(buffer, 0, length);
        }
    }

    private void cacheBodyAsString() throws IOException {

        ByteArrayOutputStream result = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024 * 50];
        int length;
        InputStream is = getInputStream();
        while ((length = is.read(buffer)) != -1) {
            result.write(buffer, 0, length);
        }

        body = result.toString("UTF-8");
    }
}

public class CachedServletInputStream extends ServletInputStream {

    private final ByteArrayInputStream buffer;

    public CachedServletInputStream(byte[] contents) {
        this.buffer = new ByteArrayInputStream(contents);
    }

    @Override
    public int read() {
        return buffer.read();
    }

    @Override
    public boolean isFinished() {
        return buffer.available() == 0;
    }

    @Override
    public boolean isReady() {
        return true;
    }

    @Override
    public void setReadListener(ReadListener listener) {
        throw new RuntimeException("Not implemented");
    }
}

This is just a sample implementation. I highly recommend to follow the steps specified in the answer mentioned above as it seems to be newer and it also ensures that the parameters are being read from both body and query string. My code is just a sample sketch to see if it works as expected.

zaerymoghaddam
  • 3,037
  • 1
  • 27
  • 33
0

Thank you @zaerymoghaddam for helping with this.

I was concerning if I am affecting the request object implicitly, so the rest of the solution is lacking something in it.

Moreover, I found that parameterMap = super.getParameterMap(); is not icluding the parameters from body (in case of post with content type of "application/x-www-form-urlencoded")

With a little bit of change of your code I came up with below solution:

public class MyRequestWrapper extends HttpServletRequestWrapper {
    private ByteArrayOutputStream cachedBytes;
    private String body;
    private Map<String, String[]> parameterMap;
    private static int bufferLength = 1024 * 50;

    public MyRequestWrapper(final HttpServletRequest request) throws IOException {
        super(request);

        cacheBodyAsString();
        
        parameterMap = new HashMap<>(super.getParameterMap());
        addParametersFromBody();
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new CachedServletInputStream(cachedBytes.toByteArray());
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(this.getInputStream()));
    }

    public String GetRequestBodyAsString() {
        return this.body;
    }
    
    @Override
    public String getParameter(String key) {
        Map<String, String[]> parameterMap = getParameterMap();
        String[] values = parameterMap.get(key);
        return values != null && values.length > 0 ? values[0] : null;
    }

    @Override
    public String[] getParameterValues(String key) {
        Map<String, String[]> parameterMap = getParameterMap();
        return parameterMap.get(key);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return parameterMap;
    }
    
    private void cacheInputStream() throws IOException {
        cachedBytes = new ByteArrayOutputStream();

        byte[] buffer = new byte[bufferLength];
        int length;
        InputStream is = super.getInputStream();
        while ((length = is.read(buffer)) != -1) {
            cachedBytes.write(buffer, 0, length);
        }
    }
    
    private void cacheBodyAsString() throws IOException {
        if (cachedBytes == null)
            cacheInputStream();
        
        this.body = cachedBytes.toString("UTF-8");
    }
    
    private void addParametersFromBody() {
        if(this.body == null || this.body.isEmpty())
            return;
        
        String[] params = this.body.split("&");

        String[] value = new String[1];
        for (String param : params) {  
            String key = param.split("=")[0];
            value[0] = param.split("=")[1];
            parameterMap.putIfAbsent(key, value);
        }

    }
    
    class CachedServletInputStream extends ServletInputStream {
        private final ByteArrayInputStream buffer;
        
        public CachedServletInputStream(byte[] contents) {
            this.buffer = new ByteArrayInputStream(contents);
        }
        
        @Override
        public int read() {
            return buffer.read();
        }

        @Override
        public boolean isFinished() {
            return buffer.available() == 0;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener listener) {
            throw new RuntimeException("Not implemented");
        }
    }
}
Reza A
  • 1
  • 2
-1

Strangely HttpServletRequest content may only be read once. It comes as a stream so once you read the stream it is gone. So you need some wrapper that allows you multiple reads. Spring actually provides such wrapper. The name of the class is ContentCachingRequestWrapper. Here its Javadoc. Here is the answer that explains how to use it if you work with Spring boot: How to get request body params in spring filter?

Michael Gantman
  • 7,315
  • 2
  • 19
  • 36