108 / Redes

HTTP: plazo y causa

Un programa que consulta una fuente externa necesita dos cosas del cliente HTTP: poder rendirse (un plazo para conectar y otro para la respuesta completa) y distinguir la causa del fallo, porque un certificado inválido y una red lenta piden reacciones distintas. try_http_request_opts recibe los plazos en un HttpOpts (armado con http_opts()) y devuelve un Error con kind/code reales: "tls" para el certificado, "timeout" para los plazos, "connection" para el puerto cerrado, "io" para una respuesta cortada a la mitad. Esta receta levanta su propio servidor local lento, así que corre sin red.

108-http-plazo-y-causa.nxFuente →
// Nyx by Example: pedir algo por HTTP sin quedarse colgado, y saber POR QUÉ falló.
//
// Un programa que consulta una fuente externa —la tasa de cambio del día, un
// modelo— necesita dos cosas del cliente HTTP:
//   1. poder rendirse: un plazo para conectar y otro para la respuesta COMPLETA;
//   2. distinguir la causa: ante un certificado inválido se deja de usar la
//      fuente, ante una red caída o lenta se reintenta más tarde.
//
// try_http_request_opts recibe los plazos en un HttpOpts (armado con http_opts())
// y devuelve un Error con kind/code reales: "tls" para el certificado,
// "timeout" para los plazos, "connection" para el puerto cerrado, "io" para una
// respuesta cortada a la mitad. try_http_get/post/request usan los valores por
// defecto (10 s para conectar, 30 s para la respuesta).
//
// La receta levanta su propio servidor local lento, así que corre sin red.

import "std/http"
import "std/net"
import "std/error"

var g_listo: Map = Map.new()

// Un servidor que responde de a un byte cada medio segundo: sin plazo total, un
// cliente se quedaría esperando todo lo que el servidor quiera.
fn servidor_lento() -> int {
    let srv: int = tcp_listen("127.0.0.1", 0)
    let puerto: Result<int, Error> = try_local_port(srv)
    channel_send(g_listo, puerto.unwrap())
    let fd: int = tcp_accept(srv)
    var linea: String = tcp_read_line(fd)
    while linea.length() > 1 { linea = tcp_read_line(fd) }
    tcp_write(fd, "HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n")
    var i: int = 0
    while i < 10 {
        sleep(500)
        tcp_write(fd, "x")
        i = i + 1
    }
    tcp_close(fd)
    return 0
}

// Qué hacer según la causa. Es la decisión que justifica que el Error tenga kind.
fn decidir(e: Error) -> String {
    if e.kind == "tls" { return "dejar de usar la fuente (certificado inválido)" }
    if e.kind == "timeout" { return "reintentar más tarde (no respondió a tiempo)" }
    if e.kind == "connection" { return "reintentar más tarde (no hay servicio)" }
    return "registrar y avisar (" + e.kind + ")"
}

fn main() {
    g_listo = channel_new(1)
    thread_spawn(servidor_lento)
    let puerto: int = channel_recv(g_listo)

    var opts: HttpOpts = http_opts()
    opts.connect_ms = 2000
    opts.respuesta_ms = 1500   // plazo TOTAL: no se reinicia con cada byte

    let t0: int = monotonic_ms()
    let sin_headers: Array = []
    let r: Result<Array, Error> = try_http_request_opts("GET", "http://127.0.0.1:" + int_to_string(puerto) + "/tasa", sin_headers, "", opts)
    let tardo: int = monotonic_ms() - t0
    match r {
        Result.Ok(resp) => { print("tasa: " + http_body(resp)) }
        Result.Err(e) => {
            print("falló con " + e.kind + " (code " + int_to_string(e.code) + ") → " + decidir(e))
        }
    }
    assert(tardo < 3000, "el plazo total cortó la espera")

    // Puerto cerrado: connection/111, sin esperar ningún plazo.
    let libre: int = tcp_listen("127.0.0.1", 0)
    let cerrado: Result<int, Error> = try_local_port(libre)
    let p: int = cerrado.unwrap()
    tcp_close(libre)
    match try_http_get("http://127.0.0.1:" + int_to_string(p) + "/") {
        Result.Ok(_resp) => { print("inesperado: respondió") }
        Result.Err(e) => { print("falló con " + e.kind + " (code " + int_to_string(e.code) + ") → " + decidir(e)) }
    }
    exit(0)
}
Salidastdout
falló con timeout (code 110) → reintentar más tarde (no respondió a tiempo)
falló con connection (code 111) → reintentar más tarde (no hay servicio)

Cómo funciona

opts.respuesta_ms = 1500 es un plazo TOTAL, no uno que se reinicia con cada byte: el servidor de la receta manda un byte cada medio segundo indefinidamente, y aun así la espera corta a los 1500 ms — el assert(tardo < 3000, ...) lo confirma. Sin ese plazo total, un cliente contra un servidor que gotea así se quedaría esperando para siempre.

decidir(e) es la función que justifica que el Error lleve kind: un certificado inválido significa dejar de usar la fuente, mientras que un timeout o una connection caída significan reintentar más tarde. Leer e.kind como un campo del valor —no como un mensaje de texto que hay que parsear— es lo que hace posible esa rama.

La segunda llamada contra un puerto que nunca escuchó nada falla inmediato con connection (código 111): no hay plazo que esperar cuando el sistema operativo rechaza la conexión al toque.