Android Upload Multiple Files In A Single Request

13.7k views Asked by At

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 ?

3

There are 3 answers

0
Ayesh Qumhieh On BEST ANSWER

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) {
    }
});
1
Patrick R On

I think you should change the MediaType for file. and you also check the link for that https://futurestud.io/tutorials/retrofit-2-how-to-upload-files-to-server and for send file with @Part MultipartBody.Part

MediaType mediaTypeForText = MediaType.parse("text/plain"); 
MediaType mediaTypeForImage = MediaType.parse("image/*");

Map<String, RequestBody> params = new HashMap<>(); 
params.put("first-parameter", MultipartBody.create(mediaTypeForText, "first-parameter-value")); 
params.put("second-parameter", MultipartBody.create(mediaTypeForText, "second-parameter-value")); 
params.put("files[0]", MultipartBody.create(mediaTypeForImage, new File(fileUri.getPath())); 
params.put("files[1]", MultipartBody.create(mediaTypeForImage, new File(fileUri.getPath()));
0
Vitaliy Rozumyak On

Hi Ayesh Qumhieh and thank you to point that we can use Array of MultipartBody.Part - that what is needed in my case to upload any amount of files attached.

In my case (kotlin) for the retrofit/okHttp3 i used next fun

@Multipart
@POST
fun attachmentUploadFiles(
    @Url url: String,
    @HeaderMap header: Map<String, String>,
    @Part multipart: Array<MultipartBody.Part>
): Call<JsonArray>

and as backend expected to get multipart/form-data and multiple files under "files" field i used next code to prepare multipart

        // preparing multipart
    val contentResolver = Application.instance.contentResolver
    val multipartList: ArrayList<MultipartBody.Part> = ArrayList()
    attachments.forEach { item ->
        multipartList.add(
            MultipartBody.Part.createFormData(
                name = "files",
                filename = item.displayName,
                body = ContentUriRequestBody( // Custom RequestBody implementation
                    contentResolver = contentResolver,
                    selectedFileInfo = item
                )
            )
        )
    }

    val result = mainRepository.attachmentUploadFiles(
        folderReference = folderReference,
        token = token,
        multipart = multipartList.toTypedArray()
    ).awaitValue()

ContentUriRequestBody - ideas is taken from there, problem is in Android if you open documents you don't have direct access to the files and you can only use stream to read file content. By next link you can get idea how to use BufferedSink - to reduce RAM memory usage when reading file by small chunks instead all file content at once. https://techenum.com/retrofit-file-upload-using-fileprovider-for-content-uris/

Thanks and hope it'll help someone also :)