Play 2.0 How to Post MultipartFormData using WS.url or WS.WSRequest
Asked Answered
C

6

8

In Java Http request, we can do this to make multipart HTTP POST.

HttpClient httpclient = new DefaultHttpClient();
HttpPost httppost = new HttpPost(url);

FileBody bin = new FileBody(new File(fileName));
StringBody comment = new StringBody("Filename: " + fileName);

MultipartEntity reqEntity = new MultipartEntity();
reqEntity.addPart("bin", bin);
reqEntity.addPart("comment", comment);
httppost.setEntity(reqEntity);

HttpResponse response = httpclient.execute(httppost);
HttpEntity resEntity = response.getEntity();

How could I achieve the same using WS.url or WS.WSRequest?

WSRequestHolder wsReq = WS.url("http//url");            
wsReq.setHeader("Content-type", "multipart/form-data");
Chaldea answered 5/6, 2012 at 1:6 Comment(0)
L
5

This is sloppy, and can definitely be cleaned up, but here's what I did to get it working. Feel free to make this so much better.

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;

import play.libs.WS;

import com.ning.http.multipart.FilePart;
import com.ning.http.multipart.MultipartRequestEntity;
import com.ning.http.multipart.Part;

ByteArrayOutputStream bos = new ByteArrayOutputStream();

// Build up the Multiparts
List<Part> parts = new ArrayList<>();
parts.add(new FilePart("file", new File(filename)));
Part[] partsA = parts.toArray(new Part[parts.size()]);

// Add it to the MultipartRequestEntity
MultipartRequestEntity reqE = new MultipartRequestEntity(partsA, null);
reqE.writeRequest(bos);
InputStream reqIS = new ByteArrayInputStream(bos.toByteArray());
WS.WSRequestHolder req = WS.url(InterchangeConfig.conflateUrl+"dataset")
    .setContentType(reqE.getContentType());
req.post(reqIS).map(...);
// or req.post(reqIS).get();

This is all using pieces already in the Play 2.0 framework.

Lieberman answered 10/9, 2013 at 15:41 Comment(6)
This worked for us in Play 2.2, but seems to have broken in Play 2.3 ... even after fixing the play.libs.ws change. Appreciate any guess why.Samuele
@EricWilson can you be more specific than "broken"?Passover
Sorry, i don't work with play any more, and don't have access to that code.Samuele
Looking at source for MultipartRequestEntity looks like it will blow up if you pass null for the headersPassover
Something similar worked for me in scala. For 2nd ctor param I write new FluentCaseInsensitiveStringsMap, and I post like: .post(baos.toByteArray())(Writeable.wBytes, ContentTypeOf(Some(mpre.getContentType)))Passover
I have created a working gist on github for play 2.3 hereEntrechat
S
3

The only solution for now, without relying to external libraries, seems to be creating the Multipart Form Data request manually. This is an example how it can be done, using play.libs.WS.url:

WSRequestHolder wsRequestHolder = WS.url(URL);

String boundary = "--XYZ123--";

String body = "";
for (String key : data.keySet()) {
  body += "--" + boundary + "\r\n"
       + "Content-Disposition: form-data; name=\""
       + key + "\"\r\n\r\n"
       + data.get(key) + "\r\n";
}
body += "--" + boundary + "--";

wsRequestHolder.setHeader("Content-Type", "multipart/form-data; boundary=" + boundary);
wsRequestHolder.setHeader("Content-length", String.valueOf(body.length()));

wsRequestHolder.post(body);

data would be a java.util.Map<String, String> containing all the name/value pairs you want to pass as form parameters. randomString is a randomized value to make the boundary change from request to request. Adding binary data would work the same way.

This http://www.htmlcodetutorial.com/forms/form_enctype.html is a good place to refer to for understanding the specs.

Semivitreous answered 27/5, 2014 at 16:49 Comment(0)
E
3

Working example for play 2.3 using above approach, also added contentType while uploading the file.

public Promise<WSResponse> upload(Http.MultipartFormData.FilePart policyFilePart, String contentType) {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    List<Part> parts = new ArrayList<>();
    try {
        parts.add(new FilePart("file", policyFilePart.getFile(), contentType, null));
        parts.add(new StringPart("param1", "value1"));
        parts.add(new StringPart("param2", "value2"));
        Part[] partsA = parts.toArray(new Part[parts.size()]);

        // Add it to the multipart request entity
        MultipartRequestEntity requestEntity = new MultipartRequestEntity(partsA, new FluentCaseInsensitiveStringsMap());
        requestEntity.writeRequest(bos);
        InputStream reqIS = new ByteArrayInputStream(bos.toByteArray());
        return WS.url(baseUrl + "upload")
                .setContentType(requestEntity.getContentType())
                .post(reqIS).map(new Function<WSResponse, WSResponse>() {
                    @Override
                    public WSResponse apply(WSResponse wsResponse) throws Throwable {
                            return wsResponse;
                    }
                });
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}
Entrechat answered 5/4, 2017 at 10:39 Comment(3)
A link to a solution is welcome, but please ensure your answer is useful without it: add context around the link so your fellow users will have some idea what it is and why it’s there, then quote the most relevant part of the page you're linking to in case the target page is unavailable. Answers that are little more than a link may be deleted.Leshalesher
@Leshalesher I have removed the link, instead I have written the code here onlyEntrechat
@Entrechat I'm trying to use this in Play 2.4 but it is resulting in 400 due to Unexpected end of inputMuskeg
D
0

It seems, based on play API documentation, that there is no built-in for multipart POST bodies.

However, it may be possible to create your own multipart body using the method

post[T](body: T)(implicit wrt: Writeable[T], ct: ContentTypeOf[T]): Future[Response]

with a type T of your choice, and the corresponding Writeable and ContentTypeOf types too.

But this would imply digging in how multipart bodies work with HTTP.

Dimarco answered 13/2, 2013 at 10:17 Comment(3)
Could you share an example?Chaldea
Actually, I've shown you the Scala API, and you seem to use Java, sorry. In Java, there's a post(InputStream) method. Maybe you can create the right content in an input stream then :)Dimarco
@angelokh: Here's an example: cdmckay.org/blog/2015/06/24/…Centenarian
C
0

As Romain Sertelon suggested, you can write a Writeable to handle this case. Here's one I wrote:

package utilities

import java.io.{ByteArrayOutputStream, File}

import com.ning.http.client.FluentCaseInsensitiveStringsMap
import com.ning.http.multipart.{MultipartRequestEntity, FilePart, StringPart}
import play.api.http.HeaderNames._
import play.api.http.{ContentTypeOf, Writeable}
import play.api.mvc.{Codec, MultipartFormData}

object MultipartFormDataWriteable {

    implicit def contentTypeOf_MultipartFormData[A](implicit codec: Codec): ContentTypeOf[MultipartFormData[A]] = {
        ContentTypeOf[MultipartFormData[A]](Some("multipart/form-data; boundary=__X_PROCESS_STREET_BOUNDARY__"))
    }

    implicit def writeableOf_MultipartFormData(implicit contentType: ContentTypeOf[MultipartFormData[File]]): Writeable[MultipartFormData[File]] = {
        Writeable[MultipartFormData[File]]((formData: MultipartFormData[File]) => {

            val stringParts = formData.dataParts flatMap {
                case (key, values) => values map (new StringPart(key, _))
            }

            val fileParts = formData.files map { filePart =>
                new FilePart(filePart.key, filePart.ref, filePart.contentType getOrElse "application/octet-stream", null)
            }

            val parts = stringParts ++ fileParts

            val headers = new FluentCaseInsensitiveStringsMap().add(CONTENT_TYPE, contentType.mimeType.get)
            val entity = new MultipartRequestEntity(parts.toArray, headers)
            val outputStream = new ByteArrayOutputStream
            entity.writeRequest(outputStream)

            outputStream.toByteArray

        })(contentType)
    }

}

Here's how to use it:

import utilities.MultipartFormDataWriteable._

...

val url = "https://example.com"

val dataParts = Map(
    "foo" -> Seq("bar"),
    "alice" -> Seq("bob")
)

val file = new jave.io.File(... path to a jpg ...)
val fileParts = Seq(new FilePart("attachment", "foo.jpg", Some("image/jpeg"), file)

val multipartFormData = MultipartFormData(dataParts, fileParts, Seq(), Seq())

WS.url(url).post(multipartFormData)
Centenarian answered 24/6, 2015 at 5:11 Comment(1)
Really great idea! It no longer compiles, though. Did you mean import com.ning.http.client.multipart ...? Also, stringParts ++ fileParts adds a StringPart to a Seq[FilePart], leading to a Seq[BasePart]. MultipartRequestEntity requires a util.List[Part] (not an Array[PartBase]). Does it compile in the latest Play release 2.4.6 for you?Celestinacelestine
F
0

The accepted answer didn't work with play 2.5. Also the answer in play 2.6 documentation didn't work for 2.5.
The below worked fine:

Http.MultipartFormData.FilePart part = new Http.MultipartFormData.FilePart("fileKey",
                "abc.zip", "multipart/form-data",
                FileIO.fromFile(new File("/home/testData/abc.zip")));
List<Http.MultipartFormData.Part<Source<ByteString, ?>>> data = Arrays.asList(part);
Http.RequestBuilder requestBuilder = AuthFakeRequest.getAuthFakeRequest(routes.MyController.uploadZip()).method(POST)
                .bodyMultipart(data, mat);
Result result = route(app, requestBuilder);

For mat and app objects they are obtained when inheriting play.test.WithApplication class.

Figured answered 9/1, 2019 at 13:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.