“This article has participated in the good article call order activity, click to see: back end, big front end double track submission, 20,000 yuan prize pool for you to challenge!”

1, the preface

This case is a real case I met. The process of finding the reason once made me break down. I believe many people have also encountered the same problem, so I write it down, hoping it will be helpful to you. This example uses RxHttp 2.6.4 + OkHttp 4.9.1 + Android Studio 4.2.2. Of course, if you use Retrofit or other okHTTP-based frameworks and use the ability to listen to upload progress, chances are you will also encounter this problem. Please be patient and if you want to see the results directly, just cross to the end of the article.

2. Problem description

The thing is, there is a file upload code, as follows:

fun uploadFiles(fileList: List<File>) {
    RxHttp.postForm("/server/...")     
        .add("key"."value")           
        .addFiles("files", fileList)   
        .upload {                       
            // Upload progress callback
        }                              
        .asString()                    
        .subscribe({                    
            // Successful callback
        }, {                            
            // Fail callback})}Copy the code

This code was ok for a long time after it was written.

This exception occurs 100% of the time. This is a very familiar exception. The reason is that the data stream is closed, but data is still written into it.

As you can see, the first line of code in the method determines whether the data stream is closed, and if so, throws an exception.

Note: Don't be surprised if you're an RxHttp user trying out this code and find it's ok, because this will only happen when executed in a specific Android Studio scenario, and it's a relatively high frequency scenario, so wait until I reveal the answer

3. Find out

In line 76 of the ProgressRequestBody class, open it as follows:

public class ProgressRequestBody extends RequestBody {

    // Omit the relevant code
    private BufferedSink bufferedSink;
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        if (bufferedSink == null) {
            bufferedSink = Okio.buffer(sink(sink));
        }
        requestBody.writeTo(bufferedSink);   // Here are 76 linesbufferedSink.flush(); }}Copy the code

The ProgressRequestBody class inherits from okHttp3. RequestBody to listen for upload progress; The last call to the ProgressRequestBody#writeTo(BufferedSink) method is on line 59 of the CallServerInterceptor interceptor

class CallServerInterceptor(private val forWebSocket: Boolean) : Interceptor {

    // Omit the relevant code
    @Throws(IOException::class)
    override fun intercept(chain: Interceptor.Chain): Response {
        // Omit the relevant code
        if (responseBuilder == null) {
            if (requestBody.isDuplex()) {
              exchange.flushRequest()
              val bufferedRequestBody = exchange.createRequestBody(request, true).buffer()
              requestBody.writeTo(bufferedRequestBody)
            } else {
              val bufferedRequestBody = exchange.createRequestBody(request, false).buffer()
              requestBody.writeTo(bufferedRequestBody)  // Here are 59 lines
              bufferedRequestBody.close()         // Close the data stream after the data is written}}}}Copy the code

The CallServerInterceptor interceptor is the last interceptor in the OkHttp interceptor chain, which provides data from the client to the server. It is implemented here, in line 59. This makes me puzzled, have no clue.

I rechecked the code to see if there was any place to manually close the data stream, but I couldn’t find it. Then, really have no way to rollback of code, roll back to first write the version of this code, I thought of anticipation, it should be no problem, try later, still submitted to Java. Lang. An IllegalStateException: I’ve been working on this issue for five hours. It’s 23:30 at night. It looks like another sleepless night.

Habit tells me, a problem for a long time did not find out, can first give up, well, unplug the hand to switch off the computer, take a bath to sleep.

Half an hour later, I was lying in bed, very uncomfortable, so I took out my phone, opened the app, and tried the upload function, surprised to find that it was ok, the upload was successful, this… I found no problem, but the problem was not found. As a junior programmer, I could not accept this.

The strength of the spirit helped me up from the bed, again open the computer, connected to the mobile phone, this time, as expected, there is a new harvest, but also suddenly refreshed my world view; When I opened the app again and tried to upload the file, the same error appeared in front of me, What?? It was fine one minute, but it’s not connected to the computer?

Ok, I completely did not temper, unplug the phone, restart the app, try again, no problem, again connected to the computer, try again, the problem came out.

At this point, I felt a little better. After all, I had a new direction of investigation. I checked the error log again and found a very strange place.

Com. Android. View profiler. Agent. Okhttp. OkHttp3Interceptor from out of nowhere? As far as I know, OkHttp3 does not have this interceptor. To verify my knowledge, check the OkHttp3 source code again, as follows:

The OkHttp3Interceptor is not provided by the CallServerInterceptor, the ConnectInterceptor is not provided by the CallServerInterceptor, the ConnectInterceptor is not provided by the CallServerInterceptor. OkHttp3Interceptor is added by the addNetworkInterceptor method, now it is easy to do, global search addNetworkInterceptor to know who added, where added, unfortunately, did not find the source code to call this method, seems to be in a dead end.

That can only open the debugging, see if OkHttp3Interceptor in OkHttpClient networkInterceptors network interceptor list of an object, a debug, indeed as expected have found, as follows:

Debug Click next, and the magic happens, as follows:

How do you explain that? Networkinterceptors. size is always 0. How does the interceptors.size increase by 1 to 5? Let’s take a look at what is added by 1, as follows:

The OkHttp3Interceptor is the one we mentioned earlier. How does this work? There is only one explanation. The OkHttpClient#networkInterceptors() method is inserted with new code by bytecode spiking. To test my idea, I did the following experiment:

As you can see, I simply create an OkHttpClient object with nothing configured, call networkInterceptors(), and get the OkHttp3Interceptor interceptor. However, the list of networkInterceptors in the OkHttpClient object does not include this interceptor, which confirms my idea.

The question now is, who injected the OkHttp3Interceptor? Is it directly related to file upload failure?

Who injected OkHttp3Interceptor?

To explore the first question, first by OkHttp3Interceptor package name of a class class com. Android. View profiler. Agent. Okhttp, I have the following 3 points

  • The package name is com.android.tools, which should be related to android official

  • The package name has agent, is also the interceptor, should be related to network proxy, that is, network monitoring

  • Last but not least, the package name is profiler, which reminds me of the Profiler network analyzer in Android Studio(AS)

Sure enough, in Google’s source code, you can find the OkHttp3Interceptor class.

public final class OkHttp3Interceptor implements Interceptor {

    // Omit the relevant code
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        Request request = chain.request();
        HttpConnectionTracker tracker = null;
        try {
            tracker = trackRequest(request);  //1, trace the request body
        } catch (Exception ex) {
            StudioLog.e("Could not track an OkHttp3 request", ex);
        }
        Response response;
        try {
            response = chain.proceed(request);
        } catch (IOException ex) {
            
        }
        try {
            if(tracker ! =null) {
                response = trackResponse(tracker, response);  //2. Trace the response body}}catch (Exception ex) {
            StudioLog.e("Could not track an OkHttp3 response", ex);
        } 
        return response;
    }
Copy the code

It is certain that it is a network monitor, but whether it is a network monitor of AS, I still doubt, because I do not have the Profiler on this project, but I have recently started the Database Inspector in the development of room Database, is this related? I tried to close the Database Inspector, restart the app, and try to upload the file again. It succeeded. I didn’t believe you, so I opened the Database Inspector again and tried to upload the file again. Next, I turned off the Database Inspector and turned on the Profiler. I tried again to upload the file, but again failed.

OkHttp3Interceptor is a network monitor in the Profiler, but there is no direct evidence that this is the case, so I try to change the ProgressRequestBody class as follows:

public class ProgressRequestBody extends RequestBody {

    // Omit the relevant code
    private BufferedSink bufferedSink;
    
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
        // If the caller is OkHttp3Interceptor, return the request body without writing it
        if (sink.toString().contains(
            "com.android.tools.profiler.support.network.HttpTracker$OutputStreamTracker"))
            return;
        if (bufferedSink == null) { bufferedSink = Okio.buffer(sink(sink)); } requestBody.writeTo(bufferedSink); bufferedSink.flush(); }}Copy the code

This is not an OkHttp3Interceptor; this is not an OkHttp3Interceptor; this is not an OkHttp3Interceptor; If the OkHttp3Interceptor is the network monitor in the Profiler, then the Profiler will not see the request body, i.e. the request parameters, as follows:

As you can see, the network monitor in the Profiler is not monitoring the request parameters.

This confirms that OkHttp3Interceptor is indeed a network monitor in the Profiler, which is dynamically injected by AS.

Does OkHttp3Interceptor have a direct relationship with file uploadings?

If you don’t have the Database Inspector and Profiler turned on, all files will be uploaded.

How does OkHttp3Interceptor affect file uploadings?

Back to the topic, how does OkHttp3Interceptor affect file uploadings? The OkHttp3Interceptor source code is provided to trace the request body:

public final class OkHttp3Interceptor implements Interceptor {

    private HttpConnectionTracker trackRequest(Request request) throws IOException {
        StackTraceElement[] callstack =
                OkHttpUtils.getCallstack(request.getClass().getPackage().getName());
        HttpConnectionTracker tracker =
                HttpTracker.trackConnection(request.url().toString(), callstack);
        tracker.trackRequest(request.method(), toMultimap(request.headers()));
        if(request.body() ! =null) {
            OutputStream outputStream =
                    tracker.trackRequestBody(OkHttpUtils.createNullOutputStream());
            BufferedSink bufferedSink = Okio.buffer(Okio.sink(outputStream));
            request.body().writeTo(bufferedSink);  // 1. Write the request body to the BufferedSink
            bufferedSink.close();                  // 2. Close BufferedSink
        }
        returntracker; }}Copy the code

So if you think about it, in the first code that I said, request.body(), you get the ProgressRequestBody object, and then you call the writeTo(BufferedSink) method, and you pass in the BufferedSink object, and the method executes, I’m going to close the BufferedSink object. However, the ProgressRequestBody declares BufferedSink as a member variable, and it’s only assigned if it’s null, This causes the CallServerInterceptor to call its writeTo(BufferedSink) method again using the last closed BufferedSink object and write data into it. Nature is Java. Lang. An IllegalStateException: closed anomalies.

4. How to solve it

Change the BufferedSink object in the ProgressRequestBody to a local variable, as follows:

public class ProgressRequestBody extends RequestBody {

    // Omit the relevant code
    @Override
    public void writeTo(BufferedSink sink) throws IOException { BufferedSink bufferedSink = Okio.buffer(sink(sink)); requestBody.writeTo(bufferedSink); bufferedSink.colse(); }}Copy the code

Ok, but there is a new problem. ProgressRequestBody is used to monitor the upload progress. The OkHttp3Interceptor and CallServerInterceptor calls their writeTo(BufferedSink) methods, which causes the request body to be written twice. What we really need is a CallServerInterceptor call. Ok, we already know if the caller is OkHttp3Interceptor

So, make the following changes:

public class ProgressRequestBody extends RequestBody {

    // Omit the relevant code
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
         // If the caller is OkHttp3Interceptor, write the request body directly instead of wrapping the class to handle the progress of the request
        if (sink.toString().contains(
        "com.android.tools.profiler.support.network.HttpTracker$OutputStreamTracker")) {
            requestBody.writeTo(bufferedSink);  
        } else{ BufferedSink bufferedSink = Okio.buffer(sink(sink)); requestBody.writeTo(bufferedSink); bufferedSink.colse(); }}}Copy the code

You think that’s the end of it? Believe that a lot of people can use com. Squareup. Okhttp3: logging – interceptor log interceptors, when you add the log interceptors, upload the file again, will find that progress callback and carried out twice, why? Because of this log interceptor, the ProgressRequestBody#writeTo(BufferedSink) method is also called. Look at the code:

// Omit part of the code
class HttpLoggingInterceptor @JvmOverloads constructor(
  private val logger: Logger = Logger.DEFAULT
) : Interceptor {

  @Throws(IOException::class)
  override fun intercept(chain: Interceptor.Chain): Response {
    val request = chain.request()
    val requestBody = request.body

    if (logHeaders) {
      if(! logBody || requestBody ==null) {
        logger.log("--> END ${request.method}")}else if (bodyHasUnknownEncoding(request.headers)) {
        logger.log("--> END ${request.method} (encoded body omitted)")}else if (requestBody.isDuplex()) {
        logger.log("--> END ${request.method} (duplex request body omitted)")}else if (requestBody.isOneShot()) {
        logger.log("--> END ${request.method} (one-shot body omitted)")}else {
        val buffer = Buffer()
        WriteTo (); writeTo ()
        requestBody.writeTo(buffer)  
      }
    }

    val response: Response
    try {
      response = chain.proceed(request)
    } catch (e: Exception) {
      throw e
    }
    return response
  }

}
Copy the code

HttpLoggingInterceptor also calls RequestBody#writeTo inside the HttpLoggingInterceptor, passing in the Buffer object. From there, we can add a Buffer to the ProgressRequestBody class, as follows:

public class ProgressRequestBody extends RequestBody {

    // Omit the relevant code
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
         // If the caller is OkHttp3Interceptor, or if the Buffer object is passed in, write the request body directly, and do not use the wrapper class to handle the progress of the request
        if (sink instanceof Buffer
            || sink.toString().contains(
            "com.android.tools.profiler.support.network.HttpTracker$OutputStreamTracker")) {
            requestBody.writeTo(bufferedSink);  
        } else{ BufferedSink bufferedSink = Okio.buffer(sink(sink)); requestBody.writeTo(bufferedSink); bufferedSink.colse(); }}}Copy the code

So that’s it? Not really. If some interceptor calls its writeTo method later, or if the progress callback is executed twice, you have to add logic to that

If so, listen to the progress callback; otherwise, write directly to the request body. It’s a good idea, and it’s feasible, as follows:

public class ProgressRequestBody extends RequestBody {

    // Omit the relevant code
    @Override
    public void writeTo(BufferedSink sink) throws IOException {
         // If the caller is CallServerInterceptor, listen for the upload progress
        if (sink.toString().contains("RequestBodySink(okhttp3.internal")) {
            BufferedSink bufferedSink = Okio.buffer(sink(sink));
            requestBody.writeTo(bufferedSink);  
            bufferedSink.colse();
        } else{ requestBody.writeTo(bufferedSink); }}}Copy the code

However, there is a fatal flaw in this scheme. If a future version of OKHTTP changes the directory structure, the ProgressRequestBody class becomes completely invalid.

The two options are up to you to choose. Here is the complete ProgressRequestBody source code

5, summary

The reason for this failure is that AS will inject new bytecode into the OkHttpClient#networkInterceptors() method using bytecode pile-in technology when the Database Inspector or Profiler is enabled. To make it more returns a com. Android. View profiler. Agent. Okhttp. OkHttp3Interceptor interceptor () is used to listening to the network, The interceptor calls the ProgressRequestBody#writeTo(BufferedSink) method and passes in the BufferedSink object. When the writeTo method completes, the BufferedSink object is immediately closed. Call the ProgressRequestBody#writeTo(BufferedSink) method to writeTo the closed BufferedSink object after the CallServerInterceptor intercepts. Eventually lead to Java. Lang. An IllegalStateException: closed.

However, there is a question that I can’t find the answer to, that is, why does the Database Inspector also cause AS to monitor the network? Anyone who knows can leave a comment in the comments section.

6. Highly recommended

RxHttp is an impressive Http request framework

RxHttp, a more elegant coroutine experience than Retrofit

RxHttp is perfect for Android 10/11 upload/download/progress monitoring

RxHttp 3K STAR, any request 3 steps to do, master the request trilogy, master the essence of RxHttp.