How to send multipart/form-data with Retrofit?
Asked Answered
D

2

34

I want to send an Article from and Android client to a REST server. Here is the Python model from the server:

class Article(models.Model):
    author = models.CharField(max_length=256, blank=False)
    photo = models.ImageField()

The following interface describes the former implementation:

@POST("/api/v1/articles/")
public Observable<CreateArticleResponse> createArticle(
        @Body Article article
);

Now I want to send an image with the Article data. The photo is not part of the Article model on the Android client.

@Multipart
@POST("/api/v1/articles/")
public Observable<CreateArticleResponse> createArticle(
        @Part("article") Article article,
        @Part("photo") TypedFile photo
);

The API is prepared and successfully tested with cURL.

$ curl -vX POST http://localhost:8000/api/v1/articles/ \
    -H "Content-Type: multipart/form-data" \
    -H "Accept:application/json" \
    -F "author=cURL" \
    -F "photo=@/home/user/Desktop/article-photo.png"

When I send data through createArticle() from the Android client I receive an HTTP 400 status stating that the fields are required/missing.

D  <--- HTTP 400 http://192.168.1.1/articles/ (2670ms)
D  Date: Mon, 20 Apr 2015 12:00:00 GMT
D  Server: WSGIServer/0.1 Python/2.7.8
D  Vary: Accept, Cookie
D  X-Frame-Options: SAMEORIGIN
D  Content-Type: application/json
D  Allow: GET, POST, HEAD, OPTIONS
D  OkHttp-Selected-Protocol: http/1.0
D  OkHttp-Sent-Millis: 1429545450469
D  OkHttp-Received-Millis: 1429545453120
D  {"author":["This field is required."],"photo":["No file was submitted."]}
D  <--- END HTTP (166-byte body)
E  400 BAD REQUEST

This is what is received as request.data on the server side:

ipdb> print request.data  
  <QueryDict: {u'article': [u'{"author":"me"}'], \
  u'photo': [<TemporaryUploadedFile: IMG_1759215522.jpg \
  (multipart/form-data)>]}>

How can convert the Article object in a multipart conform data type? I read that Retrofit might allow to use Converters for this. It should be something that implements a retrofit.mime.TypedOutput as far as I understood for the documentation.

Multipart parts use the RestAdapter's converter or they can implement TypedOutput to handle their own serialization.

Related

Damalus answered 16/4, 2015 at 16:1 Comment(6)
Isn't it the TypedFile class that can be used for this?Interlaminate
Your method appears to be fine. Why don't you enable logging on the RestAdapter and check exactly what data is being sent.Indiana
@Interlaminate Do you mean I should use TypedFile for both the JSON data (article) and the image? Please point me to how to convert the data. / @Indiana I updated my post.Damalus
Are you sure that you need multipart request? I have seen some servers that expects JSON object as POST body with image binary data posted as string. Like this: {"author":"authorNameHere","photo":"base64ImageBytesHere"]}. If this is the case I can provide the code on how to do that with Okio and Retrofit.Tench
I just saw that you have working curl request, could you post it, please?Tench
@SergiiPechenizkyi I added the cURL command. I read about the base64 option but it feels like it should be multipart/form-data ... please convince me if I am wrong.Damalus
T
28

According to your curl request you are trying to create smth like this:

POST http://localhost:8000/api/v1/articles/ HTTP/1.1
User-Agent: curl/7.30.0
Host: localhost
Connection: Keep-Alive
Accept: application/json
Content-Length: 183431
Expect: 100-continue
Content-Type: multipart/form-data; boundary=----------------------------23473c7acabb

------------------------------23473c7acabb
Content-Disposition: form-data; name="author"

cURL
------------------------------23473c7acabb
Content-Disposition: form-data; name="photo"; filename="article-photo.png"
Content-Type: application/octet-stream

‰PNG

<!RAW BYTES HERE!>

M\UUÕ+4qUUU¯°WUUU¿×ß¿þ Naa…k¿    IEND®B`‚
------------------------------23473c7acabb--

With retrofit adapter this request can be created in a next way:

@Multipart
@POST("/api/v1/articles/")
Observable<Response> uploadFile(@Part("author") TypedString authorString,
                                @Part("photo") TypedFile photoFile);

Usage:

TypedString author = new TypedString("cURL");
File photoFile = new File("/home/user/Desktop/article-photo.png");
TypedFile photoTypedFile = new TypedFile("image/*", photoFile);
retrofitAdapter.uploadFile(author, photoTypedFile)
               .subscribe(<...>);

Which creates similar output:

POST http://localhost:8000/api/v1/articles/ HTTP/1.1
Content-Type: multipart/form-data; boundary=32230279-83af-4480-abfc-88a880b21b19
Content-Length: 709
Host: localhost
Connection: Keep-Alive
Accept-Encoding: gzip
User-Agent: okhttp/2.3.0

--32230279-83af-4480-abfc-88a880b21b19
Content-Disposition: form-data; name="author"
Content-Type: text/plain; charset=UTF-8
Content-Length: 4
Content-Transfer-Encoding: binary

cUrl
--32230279-83af-4480-abfc-88a880b21b19
Content-Disposition: form-data; name="photo"; filename="article-photo.png"
Content-Type: image/*
Content-Length: 254
Content-Transfer-Encoding: binary

<!RAW BYTES HERE!>

--32230279-83af-4480-abfc-88a880b21b19--

The key difference here is that you used POJO Article article as multipart param, which by default is converted by Converter into json. And your server expects plain string instead. With curl you are sending cURL, not {"author":"cURL"}.

Tench answered 21/4, 2015 at 14:30 Comment(4)
Is it possible to convert the whole Article model into a Typed.. at once and pass it to the API instead of pulling out each member? Maybe a converter could do this?Damalus
Yes, absolutely. I haven't seen such converter, you may want to write it and tailor to your needs. Sample direction: gist.github.com/plastiv/08538a095d2d35acab05.Tench
For reference: I proposed the MultipartConverter as a feature for Retrofit.Damalus
What imports are needed for TypedString and TypedFile? Autocomplete isn't giving me any suggestions. Are these from an older version of Retrofit? If so, what are they repalced with? Or are they from another library I need to add to my dependencies?Casablanca
A
2

The server expects an "author" string but you're trying to pass it an "article" object. Pass it "String author" instead of "Article article."

Also, I think the "no file submitted" error is a red herring because the file is clearly present in your "request.data."

Adze answered 21/4, 2015 at 15:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.