Consultar una API, enviar un formulario, descargar un archivo: todo es una petición HTTP. En Kotlin para la JVM no hace falta ninguna biblioteca, porque el HttpClient de Java hace todo lo necesario. Aquí lo usamos contra un servidor de prueba que levanta el propio programa, para que puedas ejecutar cada ejemplo sin internet y ver exactamente las mismas salidas.
También te puede interesar
Lo que vas a usar: java.net.http
Desde Java 11, la JVM trae un cliente HTTP moderno en java.net.http: HttpClient envía, HttpRequest describe la petición y HttpResponse trae la respuesta. Habla HTTP/1.1 y HTTP/2, permite peticiones asíncronas y no necesita ninguna dependencia, así que desde Kotlin se usa directamente. (En Android y en Kotlin Multiplatform lo habitual es Ktor Client u OkHttp; las ideas son las mismas.)
Un servidor de pruebas en el mismo programa
Para que los ejemplos funcionen sin internet y den siempre la misma salida, cada programa levanta primero un servidor HTTP pequeño en tu propia máquina (localhost), con el HttpServer que también trae la JVM, y luego le hace las peticiones. El puerto 0 pide al sistema uno libre cualquiera. Responde JSON en cuatro rutas: /saludo, /pedidos (solo con POST), /lento (tarda tres segundos) y cualquier otra con un 404:
import com.sun.net.httpserver.HttpServer
import java.net.InetSocketAddress
import java.util.concurrent.Executors
fun servidorDePrueba(): HttpServer {
val servidor = HttpServer.create(InetSocketAddress("localhost", 0), 0)
servidor.executor = Executors.newVirtualThreadPerTaskExecutor()
servidor.createContext("/") { ex ->
val (codigo, cuerpo) = when (ex.requestURI.path) {
"/saludo" -> 200 to """{"mensaje":"Hola desde el servidor","idioma":"es-MX"}"""
"/pedidos" ->
if (ex.requestMethod == "POST") {
val recibido = ex.requestBody.readBytes().decodeToString()
201 to """{"id":1042,"recibido":$recibido}"""
} else {
405 to """{"error":"usa POST"}"""
}
"/lento" -> {
Thread.sleep(3000)
200 to """{"mensaje":"por fin"}"""
}
else -> 404 to """{"error":"no existe"}"""
}
val bytes = cuerpo.encodeToByteArray()
ex.responseHeaders.add("Content-Type", "application/json; charset=utf-8")
ex.sendResponseHeaders(codigo, bytes.size.toLong())
ex.responseBody.use { it.write(bytes) }
}
servidor.start()
return servidor
}salidaServidor de prueba en marcha
En los ejemplos de abajo, esta función va al final del archivo y no se repite. Para usarlos contra una API real, cambia base por su dirección, por ejemplo "https://api.example.com", y quita las líneas del servidor: el resto del código no cambia.
GET: pedir y leer la respuesta
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val servidor = servidorDePrueba()
val base = "http://localhost:${servidor.address.port}"
HttpClient.newHttpClient().use { cliente ->
val peticion = HttpRequest.newBuilder(URI.create("$base/saludo")).GET().build()
val respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString())
println("Código: ${respuesta.statusCode()}")
println("Tipo: ${respuesta.headers().firstValue("Content-Type").orElse("?")}")
println("Cuerpo: ${respuesta.body()}")
}
servidor.stop(0)
}salidaCódigo: 200
Tipo: application/json; charset=utf-8
Cuerpo: {"mensaje":"Hola desde el servidor","idioma":"es-MX"}El patrón es siempre el mismo: construir la petición con HttpRequest.newBuilder(uri), enviarla con send y decir cómo quieres el cuerpo con un BodyHandler (ofString() para texto, ofByteArray() o ofFile(ruta) para descargas). HttpClient es AutoCloseable desde Java 21, y use lo cierra al terminar. En un programa que hace muchas peticiones, crea un solo cliente y reutilízalo: guarda las conexiones abiertas.
POST con un cuerpo JSON
Para enviar JSON hay que convertir el objeto en texto, ponerlo con un BodyPublisher y decir el tipo en la cabecera Content-Type. Aquí la conversión la hace kotlinx.serialization, explicado en JSON en Kotlin:
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class Pedido(val producto: String, val cantidad: Int)
@Serializable
data class Respuesta(val id: Int, val recibido: Pedido)
fun main() {
val servidor = servidorDePrueba()
val base = "http://localhost:${servidor.address.port}"
HttpClient.newHttpClient().use { cliente ->
val cuerpo = Json.encodeToString(Pedido("Café de Chiapas 500 g", 3))
val peticion = HttpRequest.newBuilder(URI.create("$base/pedidos"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(cuerpo))
.build()
val respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString())
println("Enviado: $cuerpo")
println("Código: ${respuesta.statusCode()}")
val r = Json.decodeFromString<Respuesta>(respuesta.body())
println("Pedido ${r.id}: ${r.recibido.cantidad} x ${r.recibido.producto}")
}
servidor.stop(0)
}salidaEnviado: {"producto":"Café de Chiapas 500 g","cantidad":3}
Código: 201
Pedido 1042: 3 x Café de Chiapas 500 gUn 404 no es una excepción
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val servidor = servidorDePrueba()
val base = "http://localhost:${servidor.address.port}"
HttpClient.newHttpClient().use { cliente ->
for (ruta in listOf("/saludo", "/no-existe", "/pedidos")) {
val peticion = HttpRequest.newBuilder(URI.create(base + ruta)).build()
val respuesta = cliente.send(peticion, HttpResponse.BodyHandlers.ofString())
val codigo = respuesta.statusCode()
if (codigo in 200..299) {
println("$ruta -> $codigo, bien: ${respuesta.body()}")
} else {
println("$ruta -> $codigo, error: ${respuesta.body()}")
}
}
}
servidor.stop(0)
}salida/saludo -> 200, bien: {"mensaje":"Hola desde el servidor","idioma":"es-MX"}
/no-existe -> 404, error: {"error":"no existe"}
/pedidos -> 405, error: {"error":"usa POST"}send solo lanza excepciones cuando no hubo respuesta: no se pudo conectar, se agotó el tiempo, se cortó la conexión. Un 404 o un 500 son respuestas normales, con su cuerpo, y comprobar el código es trabajo tuyo. codigo in 200..299 es la forma de Kotlin de preguntar «¿fue bien?».
El tiempo de espera
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
import java.net.http.HttpTimeoutException
import java.time.Duration
fun main() {
val servidor = servidorDePrueba()
val base = "http://localhost:${servidor.address.port}"
val cliente = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build()
val peticion = HttpRequest.newBuilder(URI.create("$base/lento"))
.timeout(Duration.ofSeconds(1))
.build()
val inicio = System.nanoTime()
try {
cliente.send(peticion, HttpResponse.BodyHandlers.ofString())
} catch (e: HttpTimeoutException) {
val segundos = (System.nanoTime() - inicio) / 1_000_000_000
println("${e::class.simpleName}: ${e.message} (tras $segundos s)")
}
servidor.stop(0)
}salidaHttpTimeoutException: request timed out (tras 1 s)
HttpClient no tiene tiempo de espera por omisión para la respuesta: si el servidor acepta la conexión y luego no contesta, send se queda bloqueado indefinidamente. Hay dos tiempos distintos: connectTimeout, en el cliente, para establecer la conexión, y timeout, en cada petición, para recibir la respuesta. Pon los dos.Varias peticiones a la vez
sendAsync envía la petición y regresa enseguida con un CompletableFuture, sin esperar la respuesta. Así se pueden lanzar varias en paralelo y recoger los resultados después con join():
import java.net.URI
import java.net.http.HttpClient
import java.net.http.HttpRequest
import java.net.http.HttpResponse
fun main() {
val servidor = servidorDePrueba()
val base = "http://localhost:${servidor.address.port}"
HttpClient.newHttpClient().use { cliente ->
val rutas = listOf("/saludo", "/pedidos", "/no-existe")
val futuras = rutas.map { ruta ->
val peticion = HttpRequest.newBuilder(URI.create(base + ruta)).build()
cliente.sendAsync(peticion, HttpResponse.BodyHandlers.ofString())
}
println("Las ${futuras.size} peticiones ya salieron; ahora espero las respuestas")
for ((ruta, futura) in rutas.zip(futuras)) {
println("$ruta -> ${futura.join().statusCode()}")
}
}
servidor.stop(0)
}salidaLas 3 peticiones ya salieron; ahora espero las respuestas /saludo -> 200 /pedidos -> 405 /no-existe -> 404
En código con corrutinas, en lugar de join() (que bloquea el hilo) se usa await() de kotlinx.coroutines.future, que suspende sin bloquear; lo verás en corrutinas en paralelo.
El error de la dirección sin http://
import java.net.URI
import java.net.http.HttpRequest
fun main() {
val peticion = HttpRequest.newBuilder(URI.create("localhost:8080/saludo")).build()
println(peticion)
}salidaException in thread "main" java.lang.IllegalArgumentException: invalid URI scheme localhost
Para URI, localhost:8080/saludo es una dirección válida cuyo esquema es «localhost», y HttpRequest la rechaza porque solo acepta http y https. Escribe siempre la dirección completa.
- ParaUsa
- Crear el cliente
HttpClient.newHttpClient()onewBuilder()...build() - Una petición GET
HttpRequest.newBuilder(URI.create(url)).build() - Un POST con JSON
.header("Content-Type", "application/json").POST(BodyPublishers.ofString(json)) - Enviar y esperar
cliente.send(peticion, BodyHandlers.ofString()) - Sin esperar
cliente.sendAsync(...), luegojoin()oawait() - Código y cuerpo
respuesta.statusCode(),respuesta.body() - Limitar la espera
connectTimeouten el cliente ytimeouten la petición
Si vienes de Java: es la misma API
Es exactamente el HttpClient de Java, con la sintaxis de Kotlin: puedes comparar línea a línea con peticiones HTTP en Java.
Todo el código de esta página se compiló con Kotlin 2.4.21 y se ejecutó en la JVM de Temurin 25.0.5 en un contenedor limpio y sin red antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.