Me? I'm not.

This is my personal blog, where I write about things that interest me or that I'm currently working on.

About

I'm working as a Senior Developer for SAP SE in the S/4 HANA Source-toPay & Order-to-Cash. My main focus is the development of cloud products based on SAP Cloud Platform. I mostly do development in Java with the Spring Framework and SAPUI5 and JavaScript.

In my sparetime, I like to ride Mountain Bike and do some programming for macOS and iOS in Swift.

Spring Boot eats the body of POST requests

I my current project, I had the task to integrate Apache Olingo into our Spring Boot based service. The goal was to levarage all feature that several Spring components provide. Unfortunaltey Spring Boot ate the request bodys of POST requests with a content type of multipart/mixed. In this article, I describe the background of the issue and how I solved it.

So, why isn't the body available? I figured out, that Spring installs a filter, namely HiddenHttpMethodFilter, that consumes the request body of POST requests. After I found the reason, it was easy to fix the issue.

The first step in resolving the issue, was to define my own HiddenHttpMethodFilter:

package mypackage.name;

import mypackage.name.MultiReadHttpServletRequest;

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.filter.HiddenHttpMethodFilter;

/**
 * This configuration provides a custom {@link HiddenHttpMethodFilter} that
 * checks whether a POST request with a body of content type multipart/mixed has
 * been received. If this is the case, the request is wrapped into a
 * {@link MultiReadHttpServletRequest} request such that the body can be read
 * multiple times using the input stream.
 * Background for this is, that the default {@link HiddenHttpMethodFilter}
 * consumes the request body in case of a POST request. Hence the services don't
 * have access to the body anymore.
 */
@Configuration
public class HiddenHttpMethodFilterConfig {

  /**
   * Hidden HTTP method filter.
   *
   * @return the hidden HTTP method filter
   */
  @Bean
  public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
    return new HiddenHttpMethodFilter() {
      @Override
      protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
          FilterChain filterChain)
          throws ServletException, IOException {

        HttpServletRequest requestToUse = request;

        if ("POST".equals(request.getMethod())
            && request.getContentType() != null
            && request.getContentType().startsWith("multipart/mixed")) {
          requestToUse = new MultiReadHttpServletRequest(request);
        }

        super.doFilterInternal(requestToUse, response, filterChain);
      }
    }
  }
}

The filter checks, whether the incoming request is a POST request with a content type of multipart/part. If that is the case, it wraps the request into a request that caches the body by instantiating the class MultiReadHttpServletRequest:

package  mypackage.name;

import com.sap.cloud.lca.bps.template.config.HiddenHttpMethodFilterConfig;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;

import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.HiddenHttpMethodFilter;

/**
 * The Class MultiReadHttpServletRequest is a {@link HttpServletRequestWrapper}
 * that that caches the body of the request, such that the body can be read
 * multiple times.
 * MultiReadHttpServletRequest is used by the custom
 * {@link HiddenHttpMethodFilter} provided by
 * {@link HiddenHttpMethodFilterConfig}.
 */
public class MultiReadHttpServletRequest extends HttpServletRequestWrapper {
  private Logger logger = LoggerFactory.getLogger(MultiReadHttpServletRequest.class);
  private ByteArrayOutputStream cachedBytes;

  /**
   * This is the constructor to the MultiReadHttpServletRequest. 
   * @param request - HttpServletRequest, which handed over by the Spring Framework
   */
  public MultiReadHttpServletRequest(HttpServletRequest request) {
    super(request);
    try {
      this.cacheInputStream();
    } catch (IOException ex) {
      this.logger.error("Error while cahcing the input stream. Excpetion = {}", ex);
    }
  }

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

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

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

  private void cacheInputStream() throws IOException {
    /*
     * Cache the inputstream in order to read it multiple times. For
     * convenience, I use apache.commons IOUtils
     */
    cachedBytes = new ByteArrayOutputStream();
    IOUtils.copy(super.getInputStream(), cachedBytes);
  }

  /* An inputstream which reads the cached request body */
  private class CachedServletInputStream extends ServletInputStream {
    private ByteArrayInputStream input;

    public CachedServletInputStream(byte[] bytes) {
      /* create a new input stream from the cached request body */
      input = new ByteArrayInputStream(bytes);
    }

    @Override
    public int read() throws IOException {
      return input.read();
    }

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

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

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

When instantiating the class MultiReadHttpServletRequest, it first of all reads the entire request body and stores it in a ByteArrayOutputStream. Each time, the input stream is requested, it creates a CachedServletInputStream object and returns it. The class CachedServletInputStream receives a byte array as parameter of its constructor and wraps it in a ByteArrayInputStream.

After these changes, my REST controller, that wraps the Apache Olingo library, was able to consume the body of POST requests.