Too many temporary files when processing multipart messages in RESTEasy

Solution Verified - Updated

Environment

  • Red Hat JBoss Enterprise Application Platform (EAP)
    • 5.x
  • Microsoft Windows
  • RESTEasy

Issue

The issue is very similar to the one described in Content from issues.jboss.org is not included.RESTEASY-681 which was fixed in a later version that is not included in EAP 5.

The issue is that when processing multipart messages using the MultipartInput interface provided by RESTEasy, temporary files build up until the disk runs out of space.

Is there a way to fix this problem in EAP 5?

Resolution

Obviously upgrading to EAP 6 solves the issue, but there are a couple of known workarounds to this issue.

One solution is detailed in Content from stackoverflow.com is not included.this StackOverflow post that utilizes the Apache Commons Upload and Commons IO libraries to manually pull off the MIME attachments.

The other solution involves overriding the MIME attachment classes provided by RESTEasy to add the ability to delete the files. You need to extend MultipartInputImpl and MultipartReader in your deployment and install your MultipartReader in your JAX-RS Application so it will override the one provided by Resteasy. MultipartReader will simply install your version of MultipartInputImpl (which has the added close() method) so you can call close in your resource methods. You can replace the package com.redhat.gss.ws if you'd prefer.

Here's the code I have in my application:

MyMultipartInputImpl.java

package com.redhat.gss.ws;

import javax.ws.rs.ext.Providers;
import javax.ws.rs.core.MediaType;
import org.jboss.resteasy.plugins.providers.multipart.MultipartInputImpl;

public class MyMultipartInputImpl extends MultipartInputImpl {
  public MyMultipartInputImpl(MediaType contentType, Providers workers) {
    super(contentType, workers);
  }

  public MyMultipartInputImpl(MediaType contentType, Providers workers,
                              MediaType defaultPartContentType) {
    super(contentType, workers, defaultPartContentType);
  }

  /***** This is the added method *****/
  public void close() {
    if (mimeMessage != null) {
      try {
        mimeMessage.dispose();
      }
      catch (Exception e) {
      }
    }
  }
}

MyMultipartReader.java

package com.redhat.gss.ws;

import javax.ws.rs.Consumes;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.Providers;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

import org.jboss.resteasy.plugins.providers.multipart.MultipartReader;
import org.jboss.resteasy.plugins.providers.multipart.MultipartInputImpl;
import org.jboss.resteasy.plugins.providers.multipart.MultipartInput;

/**
 * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
 * @version $Revision: 1 $
 */
@Provider
@Consumes("multipart/*")
public class MyMultipartReader extends MultipartReader {
  protected
  @Context
  Providers workers;


  public boolean isReadable(Class<?> type, Type genericType, 
                            Annotation[] annotations, MediaType mediaType) {
    return type.equals(MultipartInput.class);
  }

  public MultipartInput readFrom(Class<MultipartInput> type, Type genericType, 
                                 Annotation[] annotations, MediaType mediaType, 
                                 MultivaluedMap<String, String> httpHeaders, 
                                 InputStream entityStream) 
                                 throws IOException, WebApplicationException {
    String boundary = mediaType.getParameters().get("boundary");
    if (boundary == null) 
      throw new IOException("Unable to get boundary for multipart");

    /***** create your custom class below *****/
    MultipartInputImpl input = new MyMultipartInputImpl(mediaType, workers);

    input.parse(entityStream);
    return input;
   }
}

MyResource.java

package com.redhat.gss.ws;

import javax.ws.rs.*;
import org.jboss.resteasy.plugins.providers.multipart.MultipartInput;
import org.jboss.resteasy.plugins.providers.multipart.InputPart;
import org.jboss.logging.Logger;

@Path("/rest")
public class MyResource {
  @POST
  @Consumes("multipart/*")
  public void readFile(MultipartInput input) {
    //Do stuff here

    /***** Add the call to close *****/
    if(input instanceof MyMultipartInputImpl) {
      ((MyMultipartInputImpl)input).close();
    }
  }
}

MyApplication.java

package com.redhat.gss.ws;

import java.util.Set;
import java.util.HashSet;

public class MyApplication extends javax.ws.rs.core.Application {
  public Set<Class<?>> getClasses() {
    Set<Class<?>> set = new HashSet<Class<?>>();
    set.add(MyResource.class);
    set.add(MyMultipartReader.class);
    return set;
  }
}

Root Cause

Mime4j opens a temporary file to keep the multipart data on the filesystem for the duration of the request. The code in mime4j that does this looks like:

public StorageOutputStream createStorageOutputStream() throws IOException {
    File file = File.createTempFile(prefix, suffix, directory);
    file.deleteOnExit();

    return new TempFileStorageOutputStream(file);
}

This means that the temporary file will be deleted when the JVM shuts down or full GC, but it won't be removed until either of those events occur. Currently the RESTEasy MultipartInput API doesn't provide a way to manually delete these temporary files when you've finished consuming the input. As you know, this was addressed in Content from issues.jboss.org is not included.RESTEASY-681, but unfortunately this was fixed in a version that's not in EAP 5. Therefore this resolution essentially provides a way to manually delete the mime4j temporary files from within your JAX-RS resource.

Category

This solution is part of Red Hat’s fast-track publication program, providing a huge library of solutions that Red Hat engineers have created while supporting our customers. To give you the knowledge you need the instant it becomes available, these articles may be presented in a raw and unedited form.