Fundamentos de OkHttp
Antes de poder llamar a Black Book, Carfax o S&P VIS de forma segura, necesitas un cliente HTTP que reutilice conexiones, tenga tiempos de espera predecibles y te permita interceptar cada solicitud.
VehicleValuationService está a punto de hacer tres tipos de llamadas salientes que no controla del todo: solicitudes a Black Book, Carfax y S&P VIS, cada una una empresa distinta con su propia disponibilidad, su propio perfil de latencia y sus propios días malos. Antes de escribir una sola línea de código específica de un proveedor, necesitas una base sólida y compartida para hacer llamadas HTTP desde la JVM — y en la JVM, esa base es casi siempre OkHttp. Es la librería sobre la que se construye Retrofit, la que la mayoría de las herramientas HTTP de Kotlin dan por sentado que usas por debajo, y es lo bastante rápida y de bajo nivel como para razonar con precisión sobre lo que ocurre en el cable.
La primera decisión, y una que es fácil tomar mal, es cuántas instancias de `OkHttpClient` crear. Un `OkHttpClient` no es un simple constructor de solicitudes ligero: crear uno pone en marcha un pool de conexiones, un dispatcher con su propio pool de hilos y cachés como la de resultados DNS. Crear uno nuevo para cada llamada a Black Book tira todo eso a la basura después de una sola solicitud, obligando a OkHttp a abrir una conexión TCP nueva y — dado que son endpoints HTTPS — renegociar un handshake TLS nuevo cada vez. La solución es construir exactamente un `OkHttpClient` para toda la aplicación, normalmente como un bean de Spring, e inyectarlo en cada cliente de proveedor. Su `ConnectionPool` mantiene entonces abiertas las conexiones keep-alive usadas recientemente, así que la segunda llamada al mismo host reutiliza una conexión ya caliente en lugar de volver a pagar los costos de establecerla.
Los tiempos de espera son lo siguiente que hay que acertar, y OkHttp te da cuatro controles independientes: `connectTimeout` (cuánto esperar el handshake TCP), `readTimeout` (cuánto esperar entre bytes una vez que la respuesta empieza a llegar), `writeTimeout` (lo mismo, para el cuerpo de la solicitud) y `callTimeout` (un tope absoluto para toda la llamada, desde la conexión hasta la respuesta, que anula a los demás). Dejar estos valores en los valores por defecto de OkHttp — diez segundos cada uno, sin tiempo de espera global para la llamada — es peligroso para un servicio como este: si Black Book tiene un mal día y cada lectura del socket se cuelga nueve segundos antes de fallar, quien llame a `VehicleController` pidiendo una valuación va a esperar mucho más de lo que cualquier contrato de API razonable debería permitir. Configura tiempos de espera agresivos y explícitos (un par de segundos para conectar, unos pocos para leer) y establece siempre `callTimeout` como respaldo, para que un proveedor con problemas nunca pueda convertir su lentitud en el problema de alguien más.
Construir una solicitud real es la parte sencilla: `Request.Builder()` toma una URL, un método, cabeceras y un cuerpo opcional, y produce un `Request` inmutable que le pasas a `client.newCall(request)`. La parte que suele confundir a la gente es la `Response` que recibes de vuelta — mantiene abierta una conexión y un cuerpo en streaming, y si no la cierras, esa conexión nunca vuelve al pool. La solución es casi siempre `response.use { ... }`, la versión de Kotlin de try-with-resources, que cierra la respuesta (y libera la conexión) tanto si tu código de parseo tiene éxito como si lanza una excepción. Olvidar esto no falla de forma ruidosa; simplemente agota poco a poco el pool de conexiones bajo carga, que es exactamente el tipo de bug que solo aparece cuando ya tienes tráfico real.
OkHttp te da dos formas de ejecutar realmente una llamada. `client.newCall(request).execute()` corre de forma síncrona y bloquea el hilo que la llama hasta que llega una respuesta (o una excepción) — simple, y la forma que usarás normalmente desde una corrutina de Kotlin. `client.newCall(request).enqueue(callback)` es asíncrono: retorna de inmediato e invoca el `onResponse` u `onFailure` de tu `Callback` más tarde, en uno de los propios hilos del dispatcher de OkHttp. Dentro de una `suspend fun`, en general no necesitas `enqueue` en absoluto — puedes llamar al `execute()` bloqueante desde dentro de `withContext(Dispatchers.IO)` y obtener el mismo comportamiento no bloqueante desde el punto de vista de la corrutina, sin escribir un solo callback. Ese es exactamente el patrón que usarán los clientes de proveedor de la próxima lección.
La última pieza, y la que se paga sola en cuanto tienes tres proveedores en lugar de uno, es el `Interceptor`. Un interceptor se sitúa en el flujo de solicitud/respuesta y puede inspeccionar, modificar, reintentar o cortocircuitar cualquier cosa que pase por él — encadena varios y cada uno envuelve al siguiente. Dos interceptores se ganan su lugar en casi cualquier servicio real: uno que estampa una cabecera de API-key compartida en cada solicitud saliente (para que ningún cliente individual tenga que acordarse de agregarla, y rotar la clave signifique cambiar un solo lugar), y `HttpLoggingInterceptor`, que registra método, URL, estado y tiempos de cada llamada que hace OkHttp. Conecta la salida del interceptor de logging al mismo pipeline que alimenta Datadog, y obtienes visibilidad de las llamadas salientes gratis, antes de haber escrito una sola línea específica de proveedor.
Nada de esto es específico de un proveedor todavía, y esa es la idea: todo lo de esta lección — el cliente compartido, sus tiempos de espera, su pool de conexiones, sus interceptores — es infraestructura sobre la que se apoyarán `BlackBookClient`, `CarfaxClient` y `VisClient` en la próxima lección. Si dejas bien esta base una sola vez, cada cliente de proveedor que escribas después será una capa delgada y específica del proveedor, en lugar de tres reimplementaciones paralelas de pooling de conexiones y manejo de tiempos de espera.
import okhttp3.ConnectionPoolimport okhttp3.OkHttpClientimport org.springframework.context.annotation.Beanimport org.springframework.context.annotation.Configurationimport java.util.concurrent.TimeUnit@Configurationclass HttpClientConfig {// One OkHttpClient for the whole application. Building a new client per// request would spin up a fresh connection pool and dispatcher thread// pool every time -- expensive, and it throws away the keep-alive// connections that make repeated calls to Black Book, Carfax, and VIS fast.@Beanfun sharedOkHttpClient(): OkHttpClient =OkHttpClient.Builder().connectTimeout(2, TimeUnit.SECONDS).readTimeout(3, TimeUnit.SECONDS).writeTimeout(3, TimeUnit.SECONDS).callTimeout(5, TimeUnit.SECONDS) // hard ceiling on the whole call.connectionPool(ConnectionPool(20, 5, TimeUnit.MINUTES)).build()}
Configures one shared, connection-pooled OkHttpClient as a Spring bean with explicit timeouts; it can't run in-browser because it needs a real JVM, Spring's ApplicationContext, and TCP/TLS sockets, none of which exist in the sandbox.
import okhttp3.Interceptorimport okhttp3.OkHttpClientimport okhttp3.Responseimport okhttp3.logging.HttpLoggingInterceptorclass ApiKeyInterceptor(private val apiKey: String) : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val authenticated = chain.request().newBuilder().header("Authorization", "Bearer $apiKey").build()return chain.proceed(authenticated)}}// Attach interceptors when the client is built. Every request that flows// through this client -- to any provider -- picks up the header and gets// logged, without every ValuationProviderClient having to remember to do it.fun buildInstrumentedClient(base: OkHttpClient.Builder, apiKey: String): OkHttpClient {val logging = HttpLoggingInterceptor().apply {level = HttpLoggingInterceptor.Level.BASIC}return base.addInterceptor(ApiKeyInterceptor(apiKey)).addInterceptor(logging).build()}
Adds a shared API-key header and request logging via interceptors so every outbound call, regardless of which provider client makes it, is authenticated and observed the same way; this needs OkHttp's real Interceptor chain and network I/O, so it can't execute in-browser.
import okhttp3.OkHttpClientimport okhttp3.Request// Synchronous execution with safe resource handling. The use { } block// guarantees the response body underlying connection is released back to// the pool even if parsing throws -- forgetting this is the single most// common OkHttp leak, and it slowly starves the connection pool under load.fun fetchRaw(client: OkHttpClient, url: String): String {val request = Request.Builder().url(url).get().build()client.newCall(request).execute().use { response ->if (!response.isSuccessful) {throw IllegalStateException("Unexpected code $response")}return response.body?.string().orEmpty()}}
Executes a request synchronously and safely closes the response with use { } to release the connection back to the pool; it can't run in-browser because it opens a real socket to a real server.
🧠 Comprueba tu comprensión
0/1 · 0/1 answered1. The valuation service builds exactly one OkHttpClient and injects it into BlackBookClient, CarfaxClient, and VisClient, rather than letting each client construct its own. What is the main reason for sharing a single instance?