// runtime/http.el — El HTTP client and server wrappers // // Thin El layer over seed primitives. All network I/O is performed by the // seed; this file provides the public API that El programs import. // // Seed primitives consumed: // __http_do(method, url, body, headers_json, timeout_ms) -> String // __http_do_to_file(method, url, body, headers_json, out_path) -> Bool // __http_do_map(method, url, body, headers_map, timeout_ms) -> String // __http_do_map_to_file(method, url, body, headers_map, out_path) -> Bool // __http_serve(port, handler_name) // __http_serve_v2(port, handler_name) // __http_response(status, headers_json, body) -> String // __env_get(key) -> String // // NOTE FOR SEED AGENT: __http_do_map and __http_do_map_to_file must be added // to the seed. They are identical to __http_do / __http_do_to_file except // they accept an ElMap directly for headers instead of a pre-serialised JSON // string. This avoids needing map iteration in El (which has no for-loop or // map iterator primitive). The seed implementation maps to headers_from_map() // in el_runtime.c. // // Other builtins used: // str_eq(a, b) -> Bool // str_to_int(s) -> Int // ── Timeout helper ──────────────────────────────────────────────────────────── // el_http_timeout_ms returns the configured HTTP timeout in milliseconds. // Reads EL_HTTP_TIMEOUT_MS from the environment; defaults to 60000 (60s). // Returns 60000 if the env var is absent, empty, or non-positive. fn el_http_timeout_ms() -> Int { let v: String = __env_get("EL_HTTP_TIMEOUT_MS") if str_eq(v, "") { return 60000 } let n: Int = str_to_int(v) if n <= 0 { return 60000 } return n } // ── HTTP client — simple variants ──────────────────────────────────────────── // http_get performs an HTTP GET request and returns the response body. // On transport failure the seed returns an error JSON fragment. fn http_get(url: String) -> String { return __http_do("GET", url, "", "{}", el_http_timeout_ms()) } // http_post performs an HTTP POST request with the given body. // No Content-Type header is set; use http_post_json for JSON payloads. fn http_post(url: String, body: String) -> String { return __http_do("POST", url, body, "{}", el_http_timeout_ms()) } // http_post_json performs an HTTP POST request with Content-Type: // application/json. body must be a valid JSON string. fn http_post_json(url: String, body: String) -> String { return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms()) } // http_delete performs an HTTP DELETE request and returns the response body. fn http_delete(url: String) -> String { return __http_do("DELETE", url, "", "{}", el_http_timeout_ms()) } // ── HTTP client — header map variants ──────────────────────────────────────── // // These accept a Map of request headers. The seed's // __http_do_map converts the ElMap to a curl_slist internally, matching // the headers_from_map() logic in el_runtime.c. // http_get_with_headers performs an HTTP GET with caller-supplied headers. fn http_get_with_headers(url: String, headers: Map) -> String { return __http_do_map("GET", url, "", headers, el_http_timeout_ms()) } // http_post_with_headers performs an HTTP POST with caller-supplied headers. fn http_post_with_headers(url: String, body: String, headers: Map) -> String { return __http_do_map("POST", url, body, headers, el_http_timeout_ms()) } // http_post_form_auth performs an HTTP POST with // Content-Type: application/x-www-form-urlencoded and an Authorization // header built from auth_header (the caller passes the full header value, // e.g. "Bearer " or "Basic "). // // Mirrors http_post_form_auth in el_runtime.c: two headers are injected, // Content-Type is always set; Authorization is omitted when auth_header is "". fn http_post_form_auth(url: String, form_body: String, auth_header: String) -> String { if str_eq(auth_header, "") { return __http_do("POST", url, form_body, "{\"Content-Type\":\"application/x-www-form-urlencoded\"}", el_http_timeout_ms()) } return __http_do("POST", url, form_body, "{\"Content-Type\":\"application/x-www-form-urlencoded\",\"Authorization\":\"" + auth_header + "\"}", el_http_timeout_ms()) } // ── HTTP client — streaming to file ────────────────────────────────────────── // // These route the response body directly to a file via the seed, bypassing // the El string layer. This preserves embedded NUL bytes in binary payloads // (audio, images, etc.) — an El string would truncate at the first NUL. // Returns true on success, false on any transport or I/O error. // http_post_to_file performs an HTTP POST and streams the response body to // output_path. Useful for large or binary response payloads. fn http_post_to_file(url: String, body: String, headers: Map, output_path: String) -> Bool { return __http_do_map_to_file("POST", url, body, headers, output_path) } // http_get_to_file performs an HTTP GET and streams the response body to // output_path. Useful for large or binary response payloads. fn http_get_to_file(url: String, headers: Map, output_path: String) -> Bool { return __http_do_map_to_file("GET", url, "", headers, output_path) } // ── HTTP server ─────────────────────────────────────────────────────────────── // // El programs call http_set_handler(name) to register which El function // handles requests, then http_serve(port, name) to start listening. // The seed resolves handler names via dlsym — every El fn compiles to a // global C symbol with the same name, so self-registration works without // any El-level registry. // // v2 widens the handler signature from // (method, path, body) -> String // to // (method, path, headers_map, body) -> String // so handlers can inspect incoming headers. Use http_serve_v2 + // http_set_handler_v2 for v2 handlers. // http_set_handler registers name as the active v1 request handler. // The seed resolves the symbol via dlsym at call time; no El-level // registration is needed. This is a no-op at the El layer. fn http_set_handler(name: String) { // no-op: the seed handles handler registration via dlsym } // http_serve starts an HTTP/1.1 server on port, dispatching every request // to handler (a v1 handler: fn(method, path, body) -> String). // Blocks forever. Accepts both IPv4 and IPv6 (dual-stack). fn http_serve(port: Int, handler: String) { __http_serve(port, handler) } // http_set_handler_v2 registers name as the active v2 request handler. // No-op at the El layer; the seed uses dlsym. fn http_set_handler_v2(name: String) { // no-op: the seed handles handler registration via dlsym } // http_serve_v2 starts an HTTP/1.1 server on port, dispatching every // request to handler (a v2 handler: fn(method, path, headers, body) -> // String). Blocks forever. Accepts both IPv4 and IPv6 (dual-stack). fn http_serve_v2(port: Int, handler: String) { __http_serve_v2(port, handler) } // ── Response construction ───────────────────────────────────────────────────── // http_response builds a structured response envelope that the HTTP server // runtime unpacks into a real HTTP response with the given status code and // headers. status must be 100–599 (defaults to 200 outside that range). // headers_json must be a JSON object literal (e.g. "{}" or // "{\"Content-Type\":\"text/html\"}"); body is the response body string. // // The envelope format is: // {"el_http_response":1,"status":,"headers":,"body":""} // The runtime detects this prefix and unpacks it; plain string returns from // handlers are still supported and are sent as HTTP 200 with auto-detected // Content-Type. fn http_response(status: Int, headers_json: String, body: String) -> String { return __http_response(status, headers_json, body) } // ── HTTP client — PATCH ─────────────────────────────────────────────────────── // http_patch performs an HTTP PATCH request with Content-Type: application/json. fn http_patch(url: String, body: String) -> String { return __http_do("PATCH", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms()) } // ── HTTP client — Engram variants (optional API key) ────────────────────────── // // These are used by dharma's db.el to talk to Engram nodes. // The key parameter is the X-API-Key header value; pass "" for no auth. // http_post_engram performs an HTTP POST with Content-Type: application/json // and an optional X-API-Key header. If key is "" no auth header is added. fn http_post_engram(url: String, key: String, body: String) -> String { if str_eq(key, "") { return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\"}", el_http_timeout_ms()) } return __http_do("POST", url, body, "{\"Content-Type\":\"application/json\",\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms()) } // http_get_engram performs an HTTP GET with an optional X-API-Key header. fn http_get_engram(url: String, key: String) -> String { if str_eq(key, "") { return __http_do("GET", url, "", "{}", el_http_timeout_ms()) } return __http_do("GET", url, "", "{\"X-API-Key\":\"" + key + "\"}", el_http_timeout_ms()) } // ── SSE — Server-Sent Events streaming ─────────────────────────────────────── // // Usage pattern for an SSE handler: // // fn my_handler(method: String, path: String, headers: Map, body: String) -> String { // let fd: Int = http_conn_fd() // http_sse_open(fd) // http_sse_send(fd, "hello") // http_sse_send(fd, "world") // http_sse_close(fd) // return http_sse_sentinel() // } // // The sentinel return value tells http_serve_v2 NOT to close the connection // automatically — the handler already closed it via http_sse_close. // http_conn_fd returns the raw file descriptor for the current HTTP connection. // Only valid inside an http_serve_v2 handler, before the handler returns. // Use with http_sse_open / http_sse_send / http_sse_close for streaming. fn http_conn_fd() -> Int { return __http_conn_fd() } // http_sse_open sends SSE response headers on the current connection, // keeping it open for streaming. Call once at the start of an SSE handler. // Returns true on success. fn http_sse_open(fd: Int) -> Bool { return __http_sse_open(fd) } // http_sse_send writes one SSE event to the connection. // data should not contain newlines (they are added automatically). // Returns true if the write succeeded (client still connected). fn http_sse_send(fd: Int, data: String) -> Bool { return __http_sse_send(fd, data) } // http_sse_close closes the SSE connection. fn http_sse_close(fd: Int) { __http_sse_close(fd) return } // http_sse_sentinel is the return value an SSE handler must return // to tell the HTTP server NOT to close the connection automatically. // The handler takes ownership of the fd and closes it via http_sse_close. fn http_sse_sentinel() -> String { return "__sse__" }