108 / Networking

HTTP: deadline and cause

A program that queries an external source needs two things from its HTTP client: the ability to give up (a deadline to connect, and another for the complete response) and a way to tell the failure's cause apart, because an invalid certificate and a slow network call for different reactions. try_http_request_opts takes the deadlines in an HttpOpts (built with http_opts()) and returns an Error with a real kind/code: "tls" for the certificate, "timeout" for the deadlines, "connection" for a closed port, "io" for a response cut off mid-way. This recipe spins up its own slow local server, so it runs without network access.

108-http-plazo-y-causa.nxSource →
// 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)
}
Outputstdout
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)

How it works

opts.respuesta_ms = 1500 is a TOTAL deadline, not one that resets with every byte: the recipe's server sends one byte every half second indefinitely, and the wait still cuts off at 1500 ms — the assert(tardo < 3000, ...) confirms it. Without that total deadline, a client against a server that dribbles bytes like this would wait forever.

decidir(e) is the function that justifies giving Error a kind: an invalid certificate means stop using the source, while a timeout or a downed connection mean retry later. Reading e.kind as a field of the value — not a text message that has to be parsed — is what makes that branch possible.

The second call, against a port that never listened for anything, fails immediately with connection (code 111): there is no deadline to wait out when the operating system rejects the connection outright.