Экстримальное использование 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 ConnectivityManager
val 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

Если вам вдруг понадобится выполнять запрос через другой сетевой движок (например, 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
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.