12

How can I track progress of upload in OkHttp 3 I can find answers for v2 but not v3, like this

A sample Multipart request from OkHttp recipes

private static final String IMGUR_CLIENT_ID = "...";
private static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");

private final OkHttpClient client = new OkHttpClient();

public void run() throws Exception {
    // Use the imgur image upload API as documented at https://api.imgur.com/endpoints/image
    RequestBody requestBody = new MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("title", "Square Logo")
            .addFormDataPart("image", "logo-square.png",
                    RequestBody.create(MEDIA_TYPE_PNG, new File("website/static/logo-square.png")))
            .build();

    Request request = new Request.Builder()
            .header("Authorization", "Client-ID " + IMGUR_CLIENT_ID)
            .url("https://api.imgur.com/3/image")
            .post(requestBody)
            .build();

    Response response = client.newCall(request).execute();
    if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);

    System.out.println(response.body().string());
}
Community
  • 1
  • 1
Sourabh
  • 8,243
  • 10
  • 52
  • 98
  • There is a recipe in the `OkHttp3` samples. It shows how to show progress of a download. If you look through it you might be able to create an upload progress monitor. Find it here https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java – Elvis Chweya Feb 20 '16 at 20:55

3 Answers3

13

You can decorate your OkHttp request body to count the number of bytes written when writing it; in order to accomplish this task, wrap your MultiPart RequestBody in this RequestBody with an instance of Listener and Voila!

public class ProgressRequestBody extends RequestBody {

    protected RequestBody mDelegate;
    protected Listener mListener;
    protected CountingSink mCountingSink;

    public ProgressRequestBody(RequestBody delegate, Listener listener) {
        mDelegate = delegate;
        mListener = listener;
    }

    @Override
    public MediaType contentType() {
        return mDelegate.contentType();
    }

    @Override
    public long contentLength() {
        try {
            return mDelegate.contentLength();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return -1;
    }

    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        mCountingSink = new CountingSink(sink);
        BufferedSink bufferedSink = Okio.buffer(mCountingSink);
        mDelegate.writeTo(bufferedSink);
        bufferedSink.flush();
    }

    protected final class CountingSink extends ForwardingSink {
        private long bytesWritten = 0;
        public CountingSink(Sink delegate) {
            super(delegate);
        }
        @Override
        public void write(Buffer source, long byteCount) throws IOException {
            super.write(source, byteCount);
            bytesWritten += byteCount;
            mListener.onProgress((int) (100F * bytesWritten / contentLength()));
        }
    }

    public interface Listener {
        void onProgress(int progress);
    }
}

Check this link for more.

Sourabh
  • 8,243
  • 10
  • 52
  • 98
  • 2
    @Saurabh the write() method is called several times over a slow network sometimes. So the actual % exceeds 100%. Have you encountered this problem ? – geekoraul Apr 18 '17 at 03:26
  • Not really, what do you mean by "several times"? How many times? 3-4 or 40-60? – Sourabh Apr 18 '17 at 09:36
  • Well its not a fixed number of times, as I mentioned, it depends on the network. So say the total % reaches 115% sometimes and sometimes it reaches 400%. The question is that have you encountered this behaviour ? – geekoraul Apr 18 '17 at 16:46
  • 3
    Could you provide example how to wrap my MultiPart RequestBody in this RequestBody? I don't know wether im doing i correctly but somehow the progress jump to 100 % in few millisecond but the actual file took about 10 second to upload. – teck wei Apr 21 '17 at 16:10
  • 1
    I found removing `HttpLoggingInterceptor` from my client stopped this from happening. – IanField90 Aug 15 '17 at 08:43
  • @teckwei I'm having the exact problem you specified. Did you find a solution? – muthuraj Sep 05 '17 at 05:00
  • @muthuraj I eventually use firebase as my backend.. But I do found the working method which you need include `Header` `RequestBody` and your `new File()` inside the addPart method else your 3rd party server will not get the file size. – teck wei Sep 05 '17 at 05:19
  • @IanField90 Which logging level did you have with `HttpLoggingInterceptor`? Also, did you try with other levels? – Louis CAD Dec 26 '17 at 10:55
  • @LouisCAD I had log level set to Body. I didn't try other levels, no. – IanField90 Jan 03 '18 at 11:40
  • @IanField90 I had a similar issue (but using binary body, not multipart) on progress, and I didn't see it again after setting logging level to headers instead of body – Louis CAD Jan 03 '18 at 11:42
  • 1
    This code example is incorrect and using it will cause your program to crash. The underlying problem is that writeTo() may be called multiple times but this class does not support that. https://github.com/square/okhttp/issues/3842#issuecomment-364898908 – Jesse Wilson Feb 12 '18 at 11:45
  • @JesseWilson How can one track progress of uploading then? Do you have a sample? – Vadim Kotov Nov 13 '19 at 12:31
  • 1
    This code does not appear to measure the progress over the wire. In my case, all the data gets buffered up front, then most of the actual upload time is spent at 100%. This code is making some pretty deep assumptions about how and when data is read from the Body. As such this is not a good stable solution. Tried it at all logging levels with no luck. – ptoinson Apr 27 '20 at 23:33
  • Feel free to add your own answer :) I believe this is a really old answer and thus deprecated, and okhttp has added a better mechanism for this – Sourabh Apr 29 '20 at 10:02
  • hi can you tel if you wrapped request body via intercepted or bofor passing body to request? – MRamzan Apr 24 '21 at 09:46
  • Hi i did via intercepter it worked thank you – MRamzan Apr 24 '21 at 09:47
6

I was unable to get any of the answers to work for me. The issue was that the progress would run to 100% before the image was uploaded hinting that some buffer was getting filled prior to the data being sent over the wire. After some research, I found this was indeed the case and that buffer was the Socket send buffer. Providing a SocketFactory to the OkHttpClient finally worked. My Kotlin code is as follows...

First, As others, I have a CountingRequestBody which is used to wrap the MultipartBody.

class CountingRequestBody(var delegate: RequestBody, private var listener: (max: Long, value: Long) -> Unit): RequestBody() {

    override fun contentType(): MediaType? {
        return delegate.contentType()
    }

    override fun contentLength(): Long {
        try {
            return delegate.contentLength()
        } catch (e: IOException) {
            e.printStackTrace()
        }
        return -1
    }

    override fun writeTo(sink: BufferedSink) {
        val countingSink = CountingSink(sink)
        val bufferedSink = Okio.buffer(countingSink)
        delegate.writeTo(bufferedSink)
        bufferedSink.flush()
    }

    inner class CountingSink(delegate: Sink): ForwardingSink(delegate) {
        private var bytesWritten: Long = 0

        override fun write(source: Buffer, byteCount: Long) {
            super.write(source, byteCount)
            bytesWritten += byteCount
            listener(contentLength(), bytesWritten)
        }
    }
}

I'm using this in Retrofit2. A general usage would be something like this:

val builder = MultipartBody.Builder()
// Add stuff to the MultipartBody via the Builder

val body = CountingRequestBody(builder.build()) { max, value ->
      // Progress your progress, or send it somewhere else.
}

At this point, I was getting progress, but I would see 100% and then a long wait while the data was uploading. The key was that the socket was, by default in my setup, configured to buffer 3145728 bytes of send data. Well, my images were just under that and the progress was showing the progress of filling that socket send buffer. To mitigate that, create a SocketFactory for the OkHttpClient.

class ProgressFriendlySocketFactory(private val sendBufferSize: Int = DEFAULT_BUFFER_SIZE) : SocketFactory() {

    override fun createSocket(): Socket {
        return setSendBufferSize(Socket())
    }

    override fun createSocket(host: String, port: Int): Socket {
        return setSendBufferSize(Socket(host, port))
    }

    override fun createSocket(host: String, port: Int, localHost: InetAddress, localPort: Int): Socket {
        return setSendBufferSize(Socket(host, port, localHost, localPort))
    }

    override fun createSocket(host: InetAddress, port: Int): Socket {
        return setSendBufferSize(Socket(host, port))
    }

    override fun createSocket(address: InetAddress, port: Int, localAddress: InetAddress, localPort: Int): Socket {
        return setSendBufferSize(Socket(address, port, localAddress, localPort))
    }

    private fun setSendBufferSize(socket: Socket): Socket {
        socket.sendBufferSize = sendBufferSize
        return socket
    }

    companion object {
        const val DEFAULT_BUFFER_SIZE = 2048
    }
}

And during config, set it.

val clientBuilder = OkHttpClient.Builder()
    .socketFactory(ProgressFriendlySocketFactory())

As others have mentioned, Logging the request body may effect this and cause the data to be read more than once. Either don't log the body, or what I do is turn it off for CountingRequestBody. To do so, I wrote my own HttpLoggingInterceptor and it solves this and other issues (like logging MultipartBody). But that's beyond the scope fo this question.

if(requestBody is CountingRequestBody) {
  // don't log the body in production
}

The other issues was with MockWebServer. I have a flavor that uses MockWebServer and json files so my app can run without a network so I can test without that burden. For this code to work, the Dispatcher needs to read the body data. I created this Dispatcher to do just that. Then it forwards the dispatch to another Dispatcher, such as the default QueueDispatcher.

class BodyReadingDispatcher(val child: Dispatcher): Dispatcher() {

    override fun dispatch(request: RecordedRequest?): MockResponse {
        val body = request?.body
        if(body != null) {
            val sink = ByteArray(1024)
            while(body.read(sink) >= 0) {
                Thread.sleep(50) // change this time to work for you
            }
        }
        val response = child.dispatch(request)
        return response
    }
}

You can use this in the MockWebServer as:

var server = MockWebServer()
server.setDispatcher(BodyReadingDispatcher(QueueDispatcher()))

This is all working code in my project. I did pull it out of illustration purposes. If it does not work for you out of the box, I apologize.

ptoinson
  • 2,013
  • 3
  • 21
  • 30
  • Good and in-depth answer! Solved my issues. I had multiple interceptors and it was easier for me to just inject a separate OkHttp instance for file uploads. – oblakr24 Oct 05 '21 at 08:12
0

According to Sourabh's answer, I want tell that field of CountingSink

private long bytesWritten = 0;

must be moved into ProgressRequestBody class

Michael Gaev
  • 69
  • 1
  • 3
  • Why, did you face any bug when it was inside `CountingSink` class? – Sourabh Aug 26 '16 at 10:58
  • I have tested this code on Android. Percentage incoming from Listener.onProgress() was in wrong order: 0, 1, 2, 0, 1, 2 and then I have got this exception: java.net.SocketException: sendto failed: ECONNRESET (Connection reset by peer) Caused by android.system.ErrnoException: sendto failed: ECONNRESET ECONNRESET (Connection reset by peer) – Michael Gaev Aug 26 '16 at 11:19
  • Please refrain from using answers to comment on other answers. This should be a comment on Sourabh's answer. – ptoinson Apr 27 '20 at 23:31