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

RealCallRealCall

Если вам вдруг понадобится выполнять запрос через другой сетевой движок (например, 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, 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

Источник: habr.com

0 0 голоса
Рейтинг новости
1
0
Подписаться
Уведомить о
0 комментариев