Экстримальное использование OkHttp

—

от автора

нейпрослоп картина, а статья нет

нейпрослоп картина, а статья нет

Обзорная статья для начинающих сетевиков, чей предыдущий опыт с OkHttp ограничивался:

okHttp.newCall(request).execute()

Балуемся с OkHttp и исследуем его возможности🍿🍦🎪🎈

База

Пробежимся по стандартным настройкам, никаких хаков

Прокси

Можем указать кастомный IP, куда польется трафик

val proxy = Proxy(Proxy.Type.HTTP, InetSocketAddress("192.168.1.1", 8080))val client = OkHttpClient.Builder().proxy(proxy).build()

Можно указать как HTTP, так и SOCKS прокси

По умолчанию берется прокси IP, установленное в системных настройках

⁉️ Вопрос на засыпку: если на девайсе подключен VPN (корпоративный, конечно), то на какой IP пойдет трафик?

Принудительный выбор сети — Wi-Fi vs Cellular

Если у юзера включен и мобильный интернет, и Wi-Fi, через что пойдет трафик?

Оказывается, можно указать самому:

val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManagerval mobileNetworkRequest = NetworkRequest.Builder()    .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)    .build()val networkCallback = object : ConnectivityManager.NetworkCallback() {    override fun onAvailable(network: Network) {        val client = OkHttpClient.Builder() // создаем OkHttp на мобильном интернете            .socketFactory(network.socketFactory)            .build()    }}connectivityManager.requestNetwork(mobileNetworkRequest, networkCallback)

⁉️ Аккуратно: пользователи могут не обрадоваться расходу мобильного интернета

Кастомный DNS-сервер

У гугл DNS опять устарели сервера и перестали отвечать? Давайте напишем свой локальный DNS-резолвер:

val client = OkHttpClient.Builder()    .dns(object : Dns {        override fun lookup(hostname: String): List<InetAddress> {            return try {                when (hostname) {                    "api.example.com" -> listOf(InetAddress.getByName("192.168.1.100"))                    "cdn.example.com" -> listOf(InetAddress.getByName("10.0.0.50"))                    else -> Dns.SYSTEM.lookup(hostname) // fallback на системный DNS                }            } catch (e: Exception) {                Dns.SYSTEM.lookup(hostname)            }        }    })    .build()

⁉️ Аккуратно: локально зашитые IP имеют свойство устаревать, нужно не забывать о механизме ротации

Мясо и треш

Не повторяйте ничего из этого в продакшен коде (без присмотра взрослых / без фича флага на откат)

Подмена сетевого слоя

В OkHttp запрос выполняется внутри стандартных OkHttp Interceptor-ов:

  • ConnectInterceptor — резолвинг DNS, установка TCP/TLS соединения

  • CallServerInterceptor — отправляем и принимает хэдеры/body

  • CacheInterceptor — чтение из кэша

  • RetryAndFollowUpInterceptor — обрабатывает редиректы и ретраи

  • BridgeInterceptor — читает cookie из CookieJar

RealCall

RealCall

Если вам вдруг понадобится выполнять запрос через другой сетевой движок (например, Cronet), можете выкинуть эти стандартные интерсепторы из цепочки и подсунуть свои

Красивый вариант — форкнуть OkHttp и заменить их в RealCall

Дешевый вариант без форка — подсунуть свои интерсепторы «перед» интерсепторами OkHttp, чтобы стандартные перестали вызываться, и оборвать в нем цепочку:

class CustomInterceptor : Interceptor {    override fun intercept(chain: Interceptor.Chain): Response {        val request = chain.request()        // Своя логика: DNS, соединение, HTTP и т. д.        val response = executeCustomRequest(request)        // Не вызываем chain.proceed()        // дальнейшие интерсепторы не вызываются        return response    }}val client = OkHttpClient.Builder()    .addInterceptor(CustomInterceptor()) // обрывает цепочку и сам выполняет запрос// дальнейшие CallServerInterceptor / ConnectInterceptor /  CacheInterceptor /  RetryAndFollowUpInterceptor не вызываются    .build()

Независимый запрос внутри Interceptor

Внутри любого интерсептора можно выполнить другой сетевой запрос:

class MyInterceptor(    private val client: OkHttpClient) : Interceptor {    override fun intercept(chain: Interceptor.Chain): Response {        ...        val response = client.newCall(request).execute()        val result = response.use { it.body.string() }        return ...    }}

⁉️ Вопрос на засыпку: если выполнить запрос внутри запроса, то какая цепочка интерсепторов выполнится в итоге?

Ретрай

Хотите написать свою retry policy?

Можно ретраить запрос внутри Interceptor с помощью повторного вызова chain.proceed(…)

class RetryInterceptor(    private val maxRetries: Int = 3) : Interceptor {    override fun intercept(chain: Interceptor.Chain): Response {        val request = chain.request()        var response: Response? = null        repeat(maxRetries + 1) { attempt ->            response?.close()            response = chain.proceed(request)            if (response!!.isSuccessful || attempt == maxRetries) return response!!        }        return response!!    }}

⁉️ Вопрос на засыпку: какие интерсепторы выполнятся, если поставить этот интерсептор в AppInterceptor‑ы? а если в NetworkInterceptor‑ы?

⁉️ Аккуратно: у OkHttp уже есть свои ретраи, включенные по умолчанию. Не переборщите

Приостановка запроса

OkHttp.Interceptor-ы не умеет в корутины, но саспендить то хочется

Например, чтобы спросить у пользователя разрешение или показать всплывашку перед произвольным запросом

Можно сделать так:

class MyInterceptor(    private val client: OkHttpClient) : Interceptor {    override fun intercept(chain: Interceptor.Chain): Response {        runBlocking {            // делаем грязь            // например, просим пользователя пройти авторизацию        }        return chain.proceed(chain.request())    }}

⁉️ Аккуратно: это блокирует поток, на котором выполняется запрос

Отключение таймаута

Знаете, что мы не учли в предыдущих разделах?

На весь запрос установлен таймаут. По умолчанию примерно 30 секунд (connectTimeout + readTimeout + writeTimeout)

И пока мы развлекаемся (ретраим или блочим запрос), часики тикают

Но это можно исправить, давайте убьем таймер:

fun Interceptor.Chain.disableTimer() = (call().timeout() as? AsyncTimeout)?.exit()

Кастомная валидация SSL-сертификатов

Через кастомный X509TrustManager можно разрешать только определенные SSL‑сертификаты (SSL‑pinning) или запрещать определенные серты по публичному ключу (например, слитые недавно eMudhra)

class CustomTrustManager(    private val blockedHash: String) : X509TrustManager {    override fun checkServerTrusted(        chain: Array<X509Certificate>,        authType: String    ) {        ...        val hash = MessageDigest.getInstance("SHA-256")            .digest(chain[0].encoded)            .joinToString("") { "%02x".format(it) }        if (hash == blockedHash) throw CertificateException("Certificate blocked")        ...    }}

⁉️ Аккуратно: важно не разрешить лишние SSL‑серты на продакшен сборках, иначе ИБ заставят переделывать. Также бывает важно проверять всю цепочку сертификатов, а не только leaf

Выводы

OkHttp отлично закрывает базовые потребности

Но под конец 2k26 по‑прежнему:

  • не поддерживает http3

  • не дает гибкого коробочного механизма настройки retry policy

  • не поддерживает корутины / suspend в интерсепторах

  • имеет неочевидное разделение на AppInterceptor‑ы (обычные) и NetworkInterceptor‑ы

  • не имеет явного разделения на Request и Response интерсепторы (в отличие от Ktor и других движков)

Надо активнее вайбкодить фичи, коллеги

Мой блог с остальными постами по теме: https://t.me/dolgo_polo_dev

ссылка на оригинал статьи https://habr.com/ru/articles/1105546/