Android Upload Multiple Files In A Single Request

11,792

First of all, I've been doing one thing wrong which is using a @PartMap Map<String, RequestBody> in the ApiDefinitions interface.

In case you're doing a post request with multiple parameters and multiple files, always make sure when defining the api method to use RequestBody for non-file parameters, and use MultipartBody.Part for file parameters.

In my case I needed to send an array of files, so the parameter that has worked for me is MultipartBody.Part[].

This is the new api definition :

@Multipart
@POST("*******/new")
Call<ApiResponse> submitNew(@Header("Authorization") String authHeader,
                                          @Part("first-parameter") RequestBody firstParameter,
                                          @Part("first-parameter") RequestBody secondParameter,
                                          @Part MultipartBody.Part[] files);

The second mistake I made was not noticing this:

PostMan Log: Content-Disposition: form-data; name="files[0]"; filename=""

Retrofit Log: Content-Disposition: form-data; name="files[0]"

The filename was not included in the multipart request, which apparently is a known issue when uploading files to a php service !

I didn't make the api, and I don't have any php background so please don't judge me. As far as I know, I was successfully sending files to the service api, but the api didn't know how to save these files !

So when sending the request:

List<Uri> files; //These are the uris for the files to be uploaded
MediaType mediaType = MediaType.parse("");//Based on the Postman logs,it's not specifying Content-Type, this is why I've made this empty content/mediaType
MultipartBody.Part[] fileParts = new MultipartBody.Part[files.size()];
for (int i = 0; i < files.size(); i++) {
    File file = new File(files.get(i).getPath());
    RequestBody fileBody = RequestBody.create(mediaType, file);
    //Setting the file name as an empty string here causes the same issue, which is sending the request successfully without saving the files in the backend, so don't neglect the file name parameter.
    fileParts[i] = MultipartBody.Part.createFormData(String.format(Locale.ENGLISH, "files[%d]", i), file.getName(), fileBody);
}

Call<ApiResponse> call = api.submitNew("Auth Token", MultipartBody.create(mediaType, "first_parameter_values"), MultipartBody.create(mediaType, "second_parameter_values"), fileParts);

call.enqueue(new Callback<ApiResponse>() {

    @Override
    public void onResponse(Call<ApiResponse> call, Response<ApiResponse> response) {
    }

    @Override
    public void onFailure(Call<ApiResponse> call, Throwable t) {
    }
});
Share:
11,792
Ayesh Qumhieh
Author by

Ayesh Qumhieh

Updated on June 24, 2022

Comments

  • Ayesh Qumhieh
    Ayesh Qumhieh almost 2 years

    I'm posting content to the server in a multipart/form-data request. and the data I'm posting contains multiple parameters including a file array parameter (files[]).

    Using postman I'm setting the parameters and the files as:

    first-parameter=text content
    second-parameter=text content
    files[0]={first selected file}
    files[1]={second selected file}
    

    Submitting this request on postman is always successful, and the files are uploaded successfully.

    When I generate code snippets on postman, this is the result:

    POST /api/*******/new HTTP/1.1
    Host: ***.***.***.*** ip-address
    Authorization: {authorization header}
    Cache-Control: no-cache
    Postman-Token: d15f13f1-4a65-81d1-bf91-f5accd1b1dd0
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
    
    ------WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="first-parameter"
    
    first-parameter-value
    ------WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="second-parameter"
    
    second-parameter-value
    ------WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="files[0]"; filename=""
    Content-Type: 
    
    ------WebKitFormBoundary7MA4YWxkTrZu0gW
    Content-Disposition: form-data; name="files[1]"; filename=""
    Content-Type: 
    
    ------WebKitFormBoundary7MA4YWxkTrZu0gW--
    

    On Android, I'm using Retrofit with the HttpLoggingInterceptor libraries, and using the @PartMap annotation when posting the request:

    //API Definition:

    interface ApiDefinitions {
    
    @Multipart
    @POST("*******/new")
    Call<ApiResponse> submitNew(@Header("Authorization") String authHeader,
                                          @PartMap Map<String, RequestBody> params);
    
    }
    

    //Preparing and sending the request code:

    public void postContent() {
        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
    
        OkHttpClient client = new OkHttpClient
                .Builder()
                .addInterceptor(interceptor)
                .build();
    
        Retrofit retrofit = new Retrofit
                .Builder()
                .baseUrl(API_URL)
                .client(client)
                .addConverterFactory(GsonConverterFactory.create(new Gson()))
                .build();
    
        ApiDefinitions api = retrofit.create(ApiDefinitions.class);
    
        MediaType mediaType = MediaType.parse("multipart/form-data");
    
        Map<String, RequestBody> params = new HashMap<>();
        params.put("first-parameter", MultipartBody.create(mediaType, "first-parameter-value"));
        params.put("second-parameter", MultipartBody.create(mediaType, "second-parameter-value"));
        params.put("files[0]", MultipartBody.create(mediaType, new File("First file path")));
        params.put("files[1]", MultipartBody.create(mediaType, new File("Second file path")));
    
        Call<ApiResponse> call = api.submitNew("Auth Token", params);
        enqueue.enqueue(new Callback<ApiResponse>() {
    
            @Override
            public void onResponse(Call<ApiResponse> call, Response<ApiResponse> response) {
            }
    
            @Override
            public void onFailure(Call<ApiResponse> call, Throwable t) {
            }
        });
    }
    

    Submitting this request works with no errors, http response is 200, But the files are not uploaded !!!

    Since I'm using the HttpLoggingInterceptor, I've tried comparing the retrofit logs with post man code snippets, this is the retrofit log:

    D/OkHttp: --> POST http://{api_address}/api/*****/new http/1.1
    D/OkHttp: Content-Type: multipart/form-data; boundary=64360751-a7f4-44c4-a008-f5de764c7298
    D/OkHttp: Content-Length: 119325
    D/OkHttp: Authorization: {Authorization-Token}
    D/OkHttp: --64360751-a7f4-44c4-a008-f5de764c7298
    D/OkHttp: Content-Disposition: form-data; name="first-parameter"
    D/OkHttp: Content-Transfer-Encoding: binary
    D/OkHttp: Content-Type: multipart/form-data; charset=utf-8
    D/OkHttp: Content-Length: 10
    D/OkHttp: first-parameter-value
    D/OkHttp: --64360751-a7f4-44c4-a008-f5de764c7298
    D/OkHttp: Content-Disposition: form-data; name="second-parameter"
    D/OkHttp: Content-Transfer-Encoding: binary
    D/OkHttp: Content-Type: multipart/form-data; charset=utf-8
    D/OkHttp: Content-Length: 10
    D/OkHttp: second-parameter-value
    D/OkHttp: --64360751-a7f4-44c4-a008-f5de764c7298
    D/OkHttp: Content-Disposition: form-data; name="files[0]"
    D/OkHttp: Content-Transfer-Encoding: binary
    D/OkHttp: Content-Type: multipart/form-data;
    D/OkHttp: Content-Length: 44205
    D/OkHttp: �PNG
    D/OkHttp: 
    D/OkHttp: ������IHDR��������������������r�B�������sBIT��O����� ��IDATx�4�˳mYv��}c�9���so�|W���R��2z�T%�8B�X� 
    D/OkHttp: &�D$��B��r�D��w�!@��������{��H���[�!,�@ �4h�P����>�A��&� ����B�[�,�fD�Mi�d�5)���5�{��-�MQt��ٗ&
    D/OkHttp: --64360751-a7f4-44c4-a008-f5de764c7298
    D/OkHttp: Content-Disposition: form-data; name="files[1]"
    D/OkHttp: Content-Transfer-Encoding: binary
    D/OkHttp: Content-Type: multipart/form-data;
    D/OkHttp: Content-Length: 44205
    D/OkHttp: �PNG
    D/OkHttp: 
    D/OkHttp: ������IHDR��������������������r�B�������sBIT��O����� ��IDATx�4�˳mYv��}c�9���so�|W���R��2z�T%�8B�X� 
    D/OkHttp: &�D$��B��r�D��w�!@��������{��H���[�!,�@ �4h�P����>�A��&� ����B�[�,�fD�Mi�d�5)���5�{��-�MQt��ٗ&
    D/OkHttp: --64360751-a7f4-44c4-a008-f5de764c7298--
    D/OkHttp: --> END POST (119325-byte body)
    D/OkHttp: <-- 200 OK http://{api_address}/api/*****/new (3409ms)
    

    I've tried to find what's wrong in my request by comparing both logs from postman vs retrofit, but I can't find it ! The random boundary strings are different which is normal, postman uses webkit, while retrofit does not ! I don't think this is an issue at all.

    I've tried to use @Part List<MultipartBody.Part instead of @PartMap Map<String, RequestBody> for the files as suggested here, but it didn't work also.

    What should I do for the files to be uploaded ?

  • Ayesh Qumhieh
    Ayesh Qumhieh over 7 years
    Although retrofit doc shows setting "multipart/form-data" as the media type for both the string parameter and the file. I've tried again with your suggestion by setting specific media-type for each part. The request is sent successfully but the files are not uploaded !