Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

OkHttp Call timeout from Retrofit Interceptor using Annotations is not applied

I'm trying to use a recently added feature from OkHttp 3.12.0: full-operation timeouts. For that, I also rely on the new Invocation class from retrofit 2.5.0 that allows me to retrieve the method annotations.

The annotation is:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Timeout {

    int value();

    TimeUnit unit();

}

The retrofit interface is:

public interface AgentApi {

    @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
    @GET("something")
    Call<String> getSomething();

}

And the interceptor is:

class TimeoutInterceptor implements Interceptor {

    @NonNull
    @Override
    public Response intercept(@NonNull Chain chain) throws IOException {
        Request request = chain.request();
        final Invocation tag = request.tag(Invocation.class);
        final Method method = tag != null ? tag.method() : null;
        final Timeout timeout = method != null ? method.getAnnotation(Timeout.class) : null;
        if (timeout != null) {
            chain.call().timeout().timeout(timeout.value(), timeout.unit());
        }
        return chain.proceed(request);
    }

}

I've correctly added the TimeoutInterceptor with .addInterceptor(...) in the OkHttpClient provided to the Retrofit Builder.

Unfortunately, it doesn't work as I expected. The calls are not failing when the timeout is reached?

Though it works fine when using the chain methods from the interceptor:

chain
  .withConnectTimeout(connect, unit)
  .withReadTimeout(read, unit)
  .withWriteTimeout(write, unit)

Is this because the call timeout must be set before the call is enqueued? (and the Interceptor is triggered too late in the process?), or is this something else?

like image 700
Simon Marquis Avatar asked Nov 21 '18 14:11

Simon Marquis


People also ask

How do I change the timeout on retrofit?

As you probably guessed, the customization of network connection timeouts on Retrofit can't be done directly. Instead, you have to configure OkHttp, Retrofit's network layer, to customize the connection timeouts: OkHttpClient okHttpClient = new OkHttpClient.

What is call timeout in OkHttp?

The call timeout spans the entire call: resolving DNS, connecting, writing the request body, server processing, and reading the response body. If the call requires redirects or retries all must complete within one timeout period. The default value is 0 which imposes no timeout. Copyright © 2022 Block, Inc.

What is interceptor in OkHttp?

Interceptors, according to the documentation, are a powerful mechanism that can monitor, rewrite, and retry the API call. So, when we make an API call, we can either monitor it or perform some tasks.

What is retrofit Readtimeout?

Configure Timeout Settings If we don't specify a client, Retrofit will create one with default connect and read timeouts. By default, Retrofit uses the following timeouts: Connection timeout: 10 seconds. Read timeout: 10 seconds. Write timeout: 10 seconds.


1 Answers

Unfortunately you are right. It is because OkHttpClient gets timeouts before it executes interceptors chain. If you look at Response execute() method in okhttp3.RealCall class you will find line timeout.enter() that is where OkHttp schedules timeouts and it is invoked before getResponseWithInterceptorChain() which is place where interceptors are executed.

Fortunately you can write workaround for that :) Put your TimeoutInterceptor in okhttp3 package (you can create that package in your app). That will allow you to have access to RealCall object which has package visibility. Your TimeoutInterceptor class should look like this:

package okhttp3;

public class TimeoutInterceptor implements Interceptor {

   @Override
   public Response intercept(Chain chain) throws IOException {
       Request request = chain.request();
       Invocation tag = request.tag(Invocation.class);
       Method method = tag != null ? tag.method() : null;
       Timeout timeout = method != null ? method.getAnnotation(Timeout.class) : null;
       if (timeout != null) {
           chain.call().timeout().timeout(timeout.value(), timeout.unit());
           RealCall realCall = (RealCall) chain.call();
           realCall.timeout.enter();
       }
       return chain.proceed(request);
   }
}

Workaround consists in executing timeout.enter() once again after changing timeout. All magic happen in lines:

RealCall realCall = (RealCall) chain.call();
realCall.timeout.enter();

Good luck!

like image 70
lukjar Avatar answered Sep 28 '22 17:09

lukjar