3
votes

After a migration from Spring Boot 2.1.10 to 2.2.4 the below method started to return null for the params parameter. It's not a bug in Spring as it works when I make a small sample project. It also works for normal GET and POST without the Content-Type: application/x-www-form-urlencoded.

@PostMapping(path = "/test", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public void test(@RequestParam Map<String, String> params) {
    System.out.println(params);
}

I fire the request below which works in one project but not the other. I've tried to disable all filters and argument resolvers, but nothing works.

curl -X POST -H "Content-Type: application/x-www-form-urlencoded" -d "param1=1&param2=2" http://localhost:8080/test

Any help or ideas would be appreciated. Also if someone can point me to the place where Spring resolves the arguments I can try to debug and see what happens.

2
POST and request params? - pks
didn't see that... may be you should look for implementation of servlet api... but that doesn't seems to be an error here. - pks

2 Answers

4
votes

Adding another answer since I ran into the same issue, but solved it in a different manner.

I too had a request logging filter, which wrapped incoming requests and cached the response input stream in a similar manner as described in the answer by Jonas Pedersen. Spring Boot was updated from 2.1.2.RELEASE to 2.3.4.RELEASE

I wrap the incoming request in a caching request wrapper which caches the input stream. For me, the issue was that for some (as of yet unknown) reason the request.getParameterValue(String key) method return null, even though the wrapped request clearly had a non-empty parameter map. Simply accessing the wrapped requests parameter map solved the issue for me...very strange.

The original wrapper class, working with Spring Boot 2.1.2.RELEASE:

public class CachingRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] cachedBody;

    public CachingRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        InputStream requestInputStream = request.getInputStream();
        this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        return new CachedInputStream(this.cachedBody);
    }

    @Override
    public BufferedReader getReader() throws IOException {
        ByteArrayInputStream byteArrayInputStream =
                new ByteArrayInputStream(this.cachedBody);
        String encoding = StringUtils.isEmpty(this.getCharacterEncoding())
                ? StandardCharsets.UTF_8.name()
                : this.getCharacterEncoding();
        return new BufferedReader(new InputStreamReader(byteArrayInputStream, encoding));
    }
}

Implementation of the CachedInputStream class is left out for brevity.

Simply accessing the wrapped request map seemed to solve the whole issue. For Spring Boot 2.3.4.RELEASE this version works (I removed some details):

public class CachingRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] cachedBody;
    private final Map<String, String[]> parameterMap;

    public CachingRequestWrapper(HttpServletRequest request) throws IOException {
        super(request);
        parameterMap = request.getParameterMap(); // <-- This was the crucial part
        InputStream requestInputStream = request.getInputStream();
        this.cachedBody = StreamUtils.copyToByteArray(requestInputStream);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return this.parameterMap; // this was added just to satisfy spotbugs
    }
}

I have not bothered to dig deeper into the issue, so I can not say what changed between the two Spring Boot versions or what accessing the parameter map in the wrappers constructor solved it.

1
votes

After countless hours of debugging, it turns out to be a problem in a logging filter. I was reading the request like this:

private static final class BufferedRequestWrapper extends HttpServletRequestWrapper {

    private final byte[] buffer;

    BufferedRequestWrapper(HttpServletRequest req) throws IOException {
        super(req);

        // Read InputStream and store its content in a buffer.
        InputStream is = req.getInputStream();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        byte[] buf = new byte[1024];
        int read;
        while ((read = is.read(buf)) > 0) {
            baos.write(buf, 0, read);
        }
        this.buffer = baos.toByteArray();
    }

    @Override
    public ServletInputStream getInputStream() {
        return new BufferedServletInputStream(new ByteArrayInputStream(this.buffer));
    }

    String getRequestBody() throws IOException {
        return IOUtils.readLines(this.getInputStream(), StandardCharsets.UTF_8).stream()
                .map(String::trim)
                .collect(Collectors.joining());
    }
}

This was consuming the query params from content-type application/x-www-form-urlencoded. My solution was to exclude reading the input stream for application/x-www-form-urlencoded.

if (req.getContentType() == null || (req.getContentType() != null && !req.getContentType().startsWith("application/x-www-form-urlencoded"))) {
    // Read InputStream and store its content in a buffer.
    InputStream is = req.getInputStream();
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    byte[] buf = new byte[1024];
    int read;
    while ((read = is.read(buf)) > 0) {
        baos.write(buf, 0, read);
    }
    this.buffer = baos.toByteArray();
} else {
    buffer = new byte[0];
}