Working POST Multipart Request with Volley and without HttpEntity
Asked Answered
N

6

113

This is not really a question, however, I would like to share some of my working code here for your reference when you need.

As we know that HttpEntity is deprecated from API22 and comletely removed since API23. At the moment, we cannot access HttpEntity Reference on Android Developer anymore (404). So, the following is my working sample code for POST Multipart Request with Volley and without HttpEntity. It's working, tested with Asp.Net Web API. Of course, the code perhaps is just a basic sample that posts two existed drawable files, also is not the best solution for all cases, and not good tuning.

MultipartActivity.java:

package com.example.multipartvolley;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import com.android.volley.NetworkResponse;
import com.android.volley.Response;
import com.android.volley.VolleyError;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;


public class MultipartActivity extends Activity {

    private final Context context = this;
    private final String twoHyphens = "--";
    private final String lineEnd = "\r\n";
    private final String boundary = "apiclient-" + System.currentTimeMillis();
    private final String mimeType = "multipart/form-data;boundary=" + boundary;
    private byte[] multipartBody;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multipart);

        byte[] fileData1 = getFileDataFromDrawable(context, R.drawable.ic_action_android);
        byte[] fileData2 = getFileDataFromDrawable(context, R.drawable.ic_action_book);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        try {
            // the first file
            buildPart(dos, fileData1, "ic_action_android.png");
            // the second file
            buildPart(dos, fileData2, "ic_action_book.png");
            // send multipart form data necesssary after file data
            dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
            // pass to multipart body
            multipartBody = bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        String url = "http://192.168.1.100/api/postfile";
        MultipartRequest multipartRequest = new MultipartRequest(url, null, mimeType, multipartBody, new Response.Listener<NetworkResponse>() {
            @Override
            public void onResponse(NetworkResponse response) {
                Toast.makeText(context, "Upload successfully!", Toast.LENGTH_SHORT).show();
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Toast.makeText(context, "Upload failed!\r\n" + error.toString(), Toast.LENGTH_SHORT).show();
            }
        });

        VolleySingleton.getInstance(context).addToRequestQueue(multipartRequest);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_multipart, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void buildPart(DataOutputStream dataOutputStream, byte[] fileData, String fileName) throws IOException {
        dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
        dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"uploaded_file\"; filename=\""
                + fileName + "\"" + lineEnd);
        dataOutputStream.writeBytes(lineEnd);

        ByteArrayInputStream fileInputStream = new ByteArrayInputStream(fileData);
        int bytesAvailable = fileInputStream.available();

        int maxBufferSize = 1024 * 1024;
        int bufferSize = Math.min(bytesAvailable, maxBufferSize);
        byte[] buffer = new byte[bufferSize];

        // read file and write it into form...
        int bytesRead = fileInputStream.read(buffer, 0, bufferSize);

        while (bytesRead > 0) {
            dataOutputStream.write(buffer, 0, bufferSize);
            bytesAvailable = fileInputStream.available();
            bufferSize = Math.min(bytesAvailable, maxBufferSize);
            bytesRead = fileInputStream.read(buffer, 0, bufferSize);
        }

        dataOutputStream.writeBytes(lineEnd);
    }

    private byte[] getFileDataFromDrawable(Context context, int id) {
        Drawable drawable = ContextCompat.getDrawable(context, id);
        Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
    }
}

MultipartRequest.java:

package com.example.multipartvolley;

import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpHeaderParser;

import java.util.Map;

class MultipartRequest extends Request<NetworkResponse> {
    private final Response.Listener<NetworkResponse> mListener;
    private final Response.ErrorListener mErrorListener;
    private final Map<String, String> mHeaders;
    private final String mMimeType;
    private final byte[] mMultipartBody;

    public MultipartRequest(String url, Map<String, String> headers, String mimeType, byte[] multipartBody, Response.Listener<NetworkResponse> listener, Response.ErrorListener errorListener) {
        super(Method.POST, url, errorListener);
        this.mListener = listener;
        this.mErrorListener = errorListener;
        this.mHeaders = headers;
        this.mMimeType = mimeType;
        this.mMultipartBody = multipartBody;
    }

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        return (mHeaders != null) ? mHeaders : super.getHeaders();
    }

    @Override
    public String getBodyContentType() {
        return mMimeType;
    }

    @Override
    public byte[] getBody() throws AuthFailureError {
        return mMultipartBody;
    }

    @Override
    protected Response<NetworkResponse> parseNetworkResponse(NetworkResponse response) {
        try {
            return Response.success(
                    response,
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (Exception e) {
            return Response.error(new ParseError(e));
        }
    }

    @Override
    protected void deliverResponse(NetworkResponse response) {
        mListener.onResponse(response);
    }

    @Override
    public void deliverError(VolleyError error) {
        mErrorListener.onErrorResponse(error);
    }
}

UPDATE:

For text part, please refer to @Oscar's answer below.

Nl answered 27/8, 2015 at 3:25 Comment(40)
I have just copied @Kevin comment at the following question: Some servers are VERY picky. If you have issues add a SPACE between ";" and "filename=" when building Content-Disposition and "multipart/form-data; boundary=" + boundary; :)Nl
if you want to add mimtype: dataOutputStream.writeBytes("Content-Type: image/jpeg" + lineEnd);Dockhand
@MaorHadad: thanks for your comment :)Nl
Thank you for this great solution. after updating to appcompat 23 this issue was pain in the a**Dockhand
Dear BNK does this work for uploading video?Anther
@mok: It may, however, Volley is not recommended for large files/payloads. I think you can try this (increase timeout with setRetryPolicy) and also other solutions such as HttpUrlConnection, OkHttp or RetrofitNl
aha, thanks. Is the order of the solutions intentional? I mean is on of them superior to others?Anther
@mok I don't mean that order :)Nl
@BNK, thanks a lot for providing great solution. I have embedded your solution into my app. I am selecting a photo in my phone library and send it via multipart form data, but it takes a couple of seconds (~10-15) before it sends to the server. Is there a way to reduce this overhead? or any other recommendation?Daunt
@Daunt because Volley actually still uses Apache library inside its classes, so if you don't want to use deprecated lib, IMO, you can try other solutions such as OkHttp, Retrofit. About the overhead, sorry I have no idea and don't know how to reproduce it to check.Nl
@casillas: If you like OkHttp, pls refer to my sample project at github.com/ngocchung/MultipartOkHttp :)Nl
@Nl can i send zip file along with some text information to server using this code?Nolasco
@MustanserIqbal: I have not tried ZIP file, however, i think if the file is not too large, this code works. You can give it a tryNl
actually i need to send 9 to 10 images at once? and one more question.I have little confusion. buildPart(dos, fileData2, "ic_action_book.png"); ["ic_action_book.png" => is this is the key which will be used to save corresponding image ]Nolasco
No, it's just filenameNl
@Nl then how i will identify the images? while receiving on server?Nolasco
@MustanserIqbal IMO, you can read here asp.net/web-api/overview/advanced/sending-html-form-data-part-2 (server-side), client-side dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"uploaded_file\"; filename=\""Nl
@Nl i tried your github code but did not upload anything on server..Nolasco
@MustanserIqbal any logcat or error message available or not? Have you tried debugging your server-side app? Did it receive any message from android client?Nl
i think the problem is on server side. because for the first time i am writing server side code and there must be something wrong.. all i get on android is upload successfully.Nolasco
@MustanserIqbal I think if you choose Asp.Net Web Api, you can refer samples at www.asp.net/webapiNl
can you share your server side code please? because still m not able to upload zip files. ThanksNolasco
@MustanserIqbal: my code is very simple pastebin.com/Vzt4wvX2, almost the same as sample code at asp.net/web-api/overview/advanced/sending-html-form-data-part-2Nl
okay thanks for the reply... let me check this.. actually i need to this in phpNolasco
@MustanserIqbal sorry I am not familiar with PHP, you can search more in SO or some link as #23504691 and sunil-android.blogspot.com/2013/09/…Nl
@Nl please take a look at my new question here https://mcmap.net/q/89357/-spinner-values-is-not-being-selected/3593066 ThanksNolasco
Awesome Thanks for sharing man, im kind of noop here so whats confius me is: in get request i know the parameter i should use from browser rrequest, but now with post how to tell what parameters does api need, spicaly the parameter for image uploadApocarpous
in this how i can add other text parrams with imageAcervate
@Acervate as I mentioned at the end, you can refer RacZo's answer below for text partsNl
is this send image as a file ? coz server need file with mutipartAcervate
@Acervate of course my solution will send image as a fileNl
what are the key for file ? name=\"uploaded_file\" is key value?Acervate
Thank you very much for the code. I tried adding the getParams() method in the code, but it's not getting called. Is there a way we can also pass parameters?Hippocrates
@MikeWalker IMO, you can try the answers belowNl
Image uploaded but not opening in windows explorer!!Tricrotic
@Nl used your whole code plus added MIME type as suggested by Maor HadadTricrotic
Thank @Nl for the great solution i will defenitely add your name in the class file's author top. Can you please tell how can be get response back from server as it only returns network response unlike String/json responseImpolite
sorry my bad i am getting the response in bytes[] String resultResponse = new String(response.data);Impolite
@Shubham so do you mean your issue has been solved?Nl
yes when i return the response from the server i get the response in the NerworkResponse object. I thought NetworkReponse will return the status code etc not the string responseImpolite
I
68

I rewrite your code @RacZo and @BNK more modular and easy to use like

VolleyMultipartRequest multipartRequest = new VolleyMultipartRequest(Request.Method.POST, url, new Response.Listener<NetworkResponse>() {
    @Override
    public void onResponse(NetworkResponse response) {
        String resultResponse = new String(response.data);
        // parse success output
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {                
        error.printStackTrace();
    }
}) {
    @Override
    protected Map<String, String> getParams() {
        Map<String, String> params = new HashMap<>();
        params.put("api_token", "gh659gjhvdyudo973823tt9gvjf7i6ric75r76");
        params.put("name", "Angga");
        params.put("location", "Indonesia");
        params.put("about", "UI/UX Designer");
        params.put("contact", "[email protected]");
        return params;
    }

    @Override
    protected Map<String, DataPart> getByteData() {
        Map<String, DataPart> params = new HashMap<>();
        // file name could found file base or direct access from real path
        // for now just get bitmap data from ImageView
        params.put("avatar", new DataPart("file_avatar.jpg", AppHelper.getFileDataFromDrawable(getBaseContext(), mAvatarImage.getDrawable()), "image/jpeg"));
        params.put("cover", new DataPart("file_cover.jpg", AppHelper.getFileDataFromDrawable(getBaseContext(), mCoverImage.getDrawable()), "image/jpeg"));

        return params;
    }
};

VolleySingleton.getInstance(getBaseContext()).addToRequestQueue(multipartRequest);

Check full of code VolleyMultipartRequest at my gist.

Inspire answered 27/4, 2016 at 13:35 Comment(5)
I don't want to convert into byte or string. for my case, server-side expecting a file and multiple texts (key-value pair, multipart form data), not the byte or string. is that possible?Exhaustive
yes it is, you can treat your data in serverside like multipart form data in web form,, actually the code is modify http header request to fit and similar web form so it is the solution you are looking for...Inspire
@AhamadullahSaikat you got anything because same thing in my project send multiple request with same nameBooklet
@AnggaAriWijaya when I upload image its showing invalid image when download it it from server. Please help I stuck badlyCelebration
@Celebration do you check uploaded file on your server? it's 0KB or missing?Inspire
L
20

Just want to add to the answer. I was trying to figure how to append text fields to the body and created the following function to do it:

private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException {
    dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
    dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + parameterName + "\"" + lineEnd);
    dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
    dataOutputStream.writeBytes(lineEnd);
    dataOutputStream.writeBytes(parameterValue + lineEnd);
}

It is working pretty well.

Leghorn answered 21/9, 2015 at 21:9 Comment(4)
I have embedded @Nl solution into my app. I am selecting a photo in my phone library and send it via multipart form data, but it takes a couple of seconds (~10-15) before it sends to the server. Is there a way to reduce this overhead? or any other recommendation?Daunt
After the deprecation of HttpEntity, multi-part uploads with volley became very cumbersome, plus implementing PATCH requests is a headache so I ended up moving away from volley and implemented RetroFit (square.github.io/retrofit) in all my apps. I would recommend you to do the same because RetroFit gives you better backward compatibility and future proofs your App.Leghorn
I agree with @RacZo, however I prefer OkHttp :), I still use Volley for other network requests. I have customized Google's volley removing Apache lib, posted to github.com/ngocchung/volleynoapache, however only tested for Get, Post and Multipart.Nl
I need to use PATCH request using Volley library. How can i acheive this.Tolyl
S
10

For those who are struggling to send utf-8 parameters and still no luck, the problem I had was in the dataOutputStream, and change the code of @RacZo to below code:

private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException {
        dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
        dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"");
        dataOutputStream.write(parameterName.getBytes("UTF-8"));
        dataOutputStream.writeBytes(lineEnd);
        dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
        dataOutputStream.writeBytes(lineEnd);
        dataOutputStream.write(parameterValue.getBytes("UTF-8"));
        dataOutputStream.writeBytes(lineEnd);
    } 
Surprise answered 8/3, 2017 at 7:57 Comment(0)
A
1

I found a wrapper of the original volley library which is easier to integrate for multi-part requests. It also supports uploading the multi-part data along with other request parameters. Hence I am sharing my code for the future developers who might run into the problem that I was having (i.e. uploading multi-part data using volley along with some other parameters).

Add the following library in the build.gradle file.

dependencies {
    compile 'dev.dworks.libs:volleyplus:+'
}

Please note that, I removed the original volley library from my build.gradle and used the above library instead which can handle both multi-part and normal requests having similar integration technique.

Then I just had to write the following class which handles the POST request operation.

public class POSTMediasTask {
    public void uploadMedia(final Context context, String filePath) {

        String url = getUrlForPOSTMedia(); // This is a dummy function which returns the POST url for you
        SimpleMultiPartRequest multiPartRequestWithParams = new SimpleMultiPartRequest(Request.Method.POST, url,
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                        Log.d("Response", response);
                        // TODO: Do something on success
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                // TODO: Handle your error here
            }
        });

        // Add the file here
        multiPartRequestWithParams.addFile("file", filePath);

        // Add the params here
        multiPartRequestWithParams.addStringParam("param1", "SomeParamValue1");
        multiPartRequestWithParams.addStringParam("param2", "SomeParamValue2");

        RequestQueue queue = Volley.newRequestQueue(context);
        queue.add(multiPartRequestWithParams);
    }
}

Now execute the task like the following.

new POSTMediasTask().uploadMedia(context, mediaPath);

You can upload one file at a time using this library. However, I could manage to upload multiple files, just by initiating multiple tasks.

Hope that helps!

Avram answered 7/6, 2019 at 16:42 Comment(0)
L
1

Here is a Kotlin version of a class allowing multipart request with Volley 1.1.1.

It's mostly based on @BNK's solution but slighly simplified. I did not notice any particular performance issue. I uploaded a 5Mb pic in about 3 seconds.

class MultipartWebservice(context: Context) {

    private var queue: RequestQueue? = null

    private val boundary = "apiclient-" + System.currentTimeMillis()
    private val mimeType = "multipart/form-data;boundary=$boundary"

    init {
        queue = Volley.newRequestQueue(context)
    }

    fun sendMultipartRequest(
        method: Int,
        url: String,
        fileData: ByteArray,
        fileName: String,
        listener: Response.Listener<NetworkResponse>,
        errorListener: Response.ErrorListener
    ) {

        // Create multi part byte array
        val bos = ByteArrayOutputStream()
        val dos = DataOutputStream(bos)
        buildMultipartContent(dos, fileData, fileName)
        val multipartBody = bos.toByteArray()

        // Request header, if needed
        val headers = HashMap<String, String>()
        headers["API-TOKEN"] = "458e126682d577c97d225bbd73a75b5989f65e977b6d8d4b2267537019ad9d20"

        val request = MultipartRequest(
            method,
            url,
            errorListener,
            listener,
            headers,
            mimeType,
            multipartBody
        )

        queue?.add(request)

    }

    @Throws(IOException::class)
    private fun buildMultipartContent(dos: DataOutputStream, fileData: ByteArray, fileName: String) {

        val twoHyphens = "--"
        val lineEnd = "\r\n"

        dos.writeBytes(twoHyphens + boundary + lineEnd)
        dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"$fileName\"$lineEnd")
        dos.writeBytes(lineEnd)
        dos.write(fileData)
        dos.writeBytes(lineEnd)
        dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd)
    }

    class MultipartRequest(
        method: Int,
        url: String,
        errorListener: Response.ErrorListener?,
        private var listener: Response.Listener<NetworkResponse>,
        private var headers: MutableMap<String, String>,
        private var mimeType: String,
        private var multipartBody: ByteArray
    ) : Request<NetworkResponse>(method, url, errorListener) {

        override fun getHeaders(): MutableMap<String, String> {
            return if (headers.isEmpty()) super.getHeaders() else headers
        }

        override fun getBodyContentType(): String {
            return mimeType
        }

        override fun getBody(): ByteArray {
            return multipartBody
        }

        override fun parseNetworkResponse(response: NetworkResponse?): Response<NetworkResponse> {
            return try {
                Response.success(response, HttpHeaderParser.parseCacheHeaders(response))
            } catch (e: Exception) {
                Response.error(ParseError(e))
            }
        }

        override fun deliverResponse(response: NetworkResponse?) {
            listener.onResponse(response)
        }
    }
}
Labrie answered 19/9, 2019 at 14:7 Comment(3)
Hi. Any more information as to how we can use the above class? For example how can we upload a .jpg image with it ?Gonfanon
Just making sure I understand how this works. Does this create a temporary file called fileName, or does it upload the file called fileName?Harner
Still unable to get this to work. As far as I can tell, this sends the entire contents in 1 piece--not multiple pieces as I expected. My server cannot accept large pieces, that's why I'm trying to use multi-part POST requests. Could you please provide more info or an example?Harner
C
1

Here is very simplified Kotlin Version for Multipart Volley POST Request

    private fun saveProfileAccount(concern: String, selectedDate: String, WDH: String, details: String, fileExtention: String) {

    val multipartRequest: VolleyMultipartRequest =
        object : VolleyMultipartRequest(
            Request.Method.POST, APIURLS.WhistleBlower,
            Response.Listener<JSONObject> { response ->

            },
            Response.ErrorListener { error ->

                error.printStackTrace()
            }) {

            @Throws(AuthFailureError::class)
            override fun getHeaders(): MutableMap<String, String>{
                val params: MutableMap<String, String> = HashMap()
                params[ConstantValues.XAppApiKey] = APIURLS.XAppApiValue
                return params
            }

            override fun getParams(): Map<String, String>? {
                val params: MutableMap<String, String> = HashMap()
                val sharedPreferences: SharedPreferences = requireActivity().getSharedPreferences(Prefs.PREF_NAME, 0)
                val userId = Prefs.getStringPref(sharedPreferences, Prefs.USER_ID)
                val userName = Prefs.getStringPref(sharedPreferences, Prefs.FULL_NAME)

                params["PersonId"] = userId.toString()
                params["PersonName"] = userName.toString()
                params["Concern"] = concern
                params["DateOfHappen"] = selectedDate
                params["WhereHappened"] = WDH
                params["Ext"] = fileExtention
                

                return params
            }


            override fun getByteData(): Map<String, DataPart>? {
                val params: MutableMap<String, DataPart> = HashMap()
                // file name could found file base or direct access from real path
                // for now just get bitmap data from ImageView

                params["cover"] = DataPart(
                    "sd.pdf",
                    byteArray,
                    "doc/pdf"
                )
                return params
            }
        }

    AppSingleton.getInstance(requireContext())?.addToRequestQueue(multipartRequest)
}
Cornett answered 14/10, 2021 at 6:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.