Limit multipart data file upload size using custom body parser Play 2.6 -> Play 2.7

In our project, we are limiting file upload size to 2.5MB using a custom body parser.
The code below worked for us in Play 2.6, but when using Play 2.7 we are seeing this error:

incompatible types: bad return type in lambda expression
[error] no instance(s) of type variable(s) A exist so that play.mvc.Http.MultipartFormData<play.libs.Files.TemporaryFile> conforms to play.mvc.Http.MultipartFormData<java.io.File>

There may be a simple solution, but I’m not seeing it. How do we update this code to work in Play 2.7?

public static class MultipartFile2500kb extends BodyParser.DelegatingBodyParser<Http.MultipartFormData<File>, MultipartFormData<Files.TemporaryFile>> {

    private static final long MAX_LENGTH = Math.round(FileUtils.ONE_MB * 2.5) + 300L;

    @Inject
    public MultipartFile2500kb(PlayBodyParsers parsers) {
        super(parsers.multipartFormData(MAX_LENGTH), JavaParsers::toJavaMultipartFormData);
    }
}

@BodyParser.Of(value = MultipartFile2500kb.class)
public Result upload() {

That happens because we switched from using File for multipart form data file uploads to TemporaryFile to represent the uploaded file. See the migration guide.

However, all you need to do is change

public static class MultipartFile2500kb extends BodyParser.DelegatingBodyParser<Http.MultipartFormData<File>, MultipartFormData<Files.TemporaryFile>> {

to

public static class MultipartFile2500kb extends BodyParser.DelegatingBodyParser<Http.MultipartFormData<play.libs.Files.TemporaryFile>, MultipartFormData<Files.TemporaryFile>> {
1 Like

With that change, I now see:

incompatible types: play.api.mvc.BodyParser<play.api.mvc.MultipartFormData<play.api.libs.Files.TemporaryFile>> cannot be converted to play.api.mvc.BodyParser<play.mvc.Http.MultipartFormData<play.libs.Files.TemporaryFile>>

I’ve also tried rewriting the parser to follow an example I found on github, but the MAX_LENGTH is not enforced. I’m able to upload an 11 MB file and the limit is supposed to be 2.5MB. If I use 1 instead of MAX_LENGTH in the constructor, I do see the 413 error as expected.

MAX_LENGTH ends up being the number 2621740

package controllers.bodyparsers;

import akka.stream.IOResult;
import akka.stream.Materializer;
import akka.stream.javadsl.FileIO;
import akka.stream.javadsl.Sink;
import akka.util.ByteString;
import org.apache.commons.io.FileUtils;
import play.api.http.HttpErrorHandler;
import play.core.parsers.Multipart;
import play.libs.streams.Accumulator;
import play.mvc.BodyParser;
import play.mvc.Http;

import javax.inject.Inject;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.concurrent.CompletionStage;
import java.util.function.Function;

/**
 * Making own version of {@link play.mvc.BodyParser.MultipartFormData} to pass in max length
 * <p>
 * Accept only 2500KB of data + small overhead
 * https://github.com/playframework/play-samples/blob/79639f2914254ad65a604e1ff388ca4641e33922/play-java-fileupload-example/README.md
 */
public class MultipartFile2500kb extends BodyParser.DelegatingMultipartFormDataBodyParser {

    /**
     * 2.5mb + 300 bytes overhead for multipart data
     */
    private static final long MAX_LENGTH = Math.round(FileUtils.ONE_MB * 2.5) + 300L;

    @Inject
    public MultipartFile2500kb(Materializer materializer, HttpErrorHandler errorHandler) {
        super(materializer, MAX_LENGTH, errorHandler);
    }

    @Override
    public Function<Multipart.FileInfo, Accumulator<ByteString, Http.MultipartFormData.FilePart>> createFilePartHandler() {
        return (Multipart.FileInfo fileInfo) -> {
            final String filename = fileInfo.fileName();
            final String partname = fileInfo.partName();
            final String contentType = fileInfo.contentType().getOrElse(null);
            final File file = generateTempFile();

            final Sink<ByteString, CompletionStage<IOResult>> sink = FileIO.toFile(file);
            return Accumulator.fromSink(
                    sink.mapMaterializedValue(completionStage ->
                            completionStage.thenApplyAsync(results -> {
                                //noinspection unchecked
                                return new Http.MultipartFormData.FilePart(partname,
                                        filename,
                                        contentType,
                                        file);
                            })
                    ));
        };
    }

    /**
     * Generates a temp file directly without going through TemporaryFile.
     */
    private File generateTempFile() {
        try {
            final Path path = java.nio.file.Files.createTempFile("multipartBody", "tempFile");
            return path.toFile();
        } catch (IOException e) {
            throw new IllegalStateException(e);
        }
    }
}

Ok, the key was converting the java-side play.libs.Files.TemporaryFile into the scala-side play.api.libs.Files.TemporaryFile as shown below. Matthias’ solution is correct, but I had the wrong imports. Explicitly mapping the packages in the class header is more fool-proof.

public class MultipartFile2500kb extends BodyParser.DelegatingBodyParser<Http.MultipartFormData<play.libs.Files.TemporaryFile>, MultipartFormData<play.api.libs.Files.TemporaryFile>> {

    private static final int MAX_LENGTH = (int) Math.round(FileUtils.ONE_MB * 2.5) + 300;

    @Inject
    public MultipartFile2500kb(PlayBodyParsers parsers) {
        super(parsers.multipartFormData(MAX_LENGTH), JavaParsers::toJavaMultipartFormData);
    }
}

Good to see it worked for you :+1:

1 Like