Advanced Customization of OkHttp: Request Retries, Caching Strategies, and DNS Optimization for Mastering Network Requests!

Advanced Customization of OkHttp: Request Retries, Caching Strategies, and DNS Optimization for Mastering Network Requests!

Last year during the Double Eleven shopping festival, our e-commerce app suddenly experienced a large number of network request timeouts during peak hours, leading to a flood of user complaints about product pages not loading. After urgent investigation, we found that the default OkHttp configuration could not handle high concurrency scenarios at all. That night, I completely re-evaluated the customization power of OkHttp.

Request Retries: Not All Failures Are Worth Retrying

Most developers’ understanding of OkHttp’s retry mechanism remains superficial. You might know about retryOnConnectionFailure(true), but do you really understand when it retries and when it does not?

The biggest pitfall I encountered was blindly enabling retries. Once, an interface occasionally returned a 500 error, and I naively thought that adding a retry would solve it:

OkHttpClient client = new OkHttpClient.Builder()
    .retryOnConnectionFailure(true)
    .build();

It turned out that 500 errors do not trigger retries at all! OkHttp only retries on connection failures; it does not retry on HTTP error codes. To implement a truly useful retry strategy, you need to create your own Interceptor:

public class SmartRetryInterceptor implements Interceptor {
    private final int maxRetryCount;
    private final Set<Integer> retryableCodes;
    
    public SmartRetryInterceptor(int maxRetryCount) {
        this.maxRetryCount = maxRetryCount;
        // Only retry for these status codes to avoid blind retries on 4xx client errors
        this.retryableCodes = Set.of(500, 502, 503, 504, 408);
    }
    
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = null;
        int retryCount = 0;
        
        while (retryCount <= maxRetryCount) {
            try {
                response = chain.proceed(request);
                // Return on success or non-retryable error codes
                if (response.isSuccessful() || !retryableCodes.contains(response.code())) {
                    return response;
                }
                response.close(); // Remember to close to avoid connection leaks
            } catch (IOException e) {
                if (retryCount == maxRetryCount) {
                    throw e;
                }
            }
            
            retryCount++;
            // Exponential backoff to avoid avalanche
            try {
                Thread.sleep(Math.min(1000 * (1L << retryCount), 10000));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                throw new IOException("Retry interrupted", e);
            }
        }
        
        return response;
    }
}

There is a particularly important detail here: exponential backoff. I have seen too many colleagues directly use Thread.sleep(1000) for fixed interval retries, resulting in all clients retrying together when the server is under heavy load, which can crash the server.

Caching Strategies: Make Your App Lightning Fast

When it comes to caching, most people’s first reaction is CacheControl.FORCE_CACHE. But have you ever wondered why some interfaces, despite having caching set, still request the network every time?

I was troubled by this issue for a long time. Debugging revealed that the Cache-Control field was completely missing from the headers returned by the server! Without a cache header, OkHttp will not cache.

At this point, we need to take proactive measures to force the response to include cache headers:

public class ForceCacheInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);
        
        // Force caching for specific interfaces
        if (shouldCache(request.url().toString())) {
            return response.newBuilder()
                .header("Cache-Control", "public, max-age=300") // Cache for 5 minutes
                .build();
        }
        
        return response;
    }
    
    private boolean shouldCache(String url) {
        // Relatively stable data like product details and user information
        return url.contains("/api/product/") || 
               url.contains("/api/user/profile");
    }
}

But that’s not enough; in actual projects, you may need more refined caching strategies. For example, cache user information for 10 minutes, product information for 1 hour, and homepage data for only 30 seconds.

The most elegant way is to declare caching strategies using custom annotations:

// Annotate on Retrofit interface
@GET("/api/product/{id}")
@CacheStrategy(maxAge = 3600) // Cache for 1 hour
Call<Product> getProduct(@Path("id") String productId);

The tricky part is that the default cache size on Android is only 5MB, which is not enough for apps with many images. Remember to increase the cache size:

File cacheDir = new File(context.getCacheDir(), "http_cache");
Cache cache = new Cache(cacheDir, 50 * 1024 * 1024); // 50MB

OkHttpClient client = new OkHttpClient.Builder()
    .cache(cache)
    .build();

DNS Optimization: Solving Those Mysterious Network Issues

DNS hijacking, DNS pollution, slow DNS resolution… these issues are particularly common in domestic network environments. I vividly remember one instance where users reported that the app could not connect to the server in certain regions, and packet capture revealed that the DNS resolution IP was completely wrong!

OkHttp provides the ability to customize DNS, and we can use HttpDNS to bypass the carrier’s DNS:

public class HttpDnsResolver implements Dns {
    private final Dns systemDns = Dns.SYSTEM;
    private final String httpDnsUrl = "https://your-httpdns-provider.com/resolve";
    
    @Override
    public List<InetAddress> lookup(String hostname) throws UnknownHostException {
        // First try HttpDNS
        try {
            return lookupWithHttpDns(hostname);
        } catch (Exception e) {
            // HttpDNS failed, fallback to system DNS
            return systemDns.lookup(hostname);
        }
    }
    
    private List<InetAddress> lookupWithHttpDns(String hostname) throws Exception {
        // Implement HttpDNS query logic here
        // In actual projects, it is recommended to use mature HttpDNS services like Alibaba Cloud or Tencent Cloud
        OkHttpClient dnsClient = new OkHttpClient.Builder()
            .connectTimeout(3, TimeUnit.SECONDS)
            .build();
            
        Request request = new Request.Builder()
            .url(httpDnsUrl + "?host=" + hostname)
            .build();
            
        Response response = dnsClient.newCall(request).execute();
        // Parse the returned IP addresses...
        return parseIpAddresses(response.body().string());
    }
}

In addition to HttpDNS, a small trick is to pre-resolve commonly used domain names. Resolve the main domain names when the app starts, so subsequent requests can save DNS query time:

// Pre-warm DNS at app startup
CompletableFuture.runAsync(() -> {
    try {
        Dns.SYSTEM.lookup("api.yourapp.com");
        Dns.SYSTEM.lookup("cdn.yourapp.com");
    } catch (Exception e) {
        // ignore
    }
});

After optimizing these three aspects, the success rate of our app’s network requests increased from 95% to 99.2%, and the average response time decreased by 40%. Users no longer complain about network issues to the product manager.

Finally, I want to say that network optimization has no silver bullet; it only requires precise targeting for specific scenarios. Monitoring data will tell you where the bottlenecks are; do not optimize just for the sake of optimizing.

Leave a Comment