From fea830cca2877c293dd4415ad01fc1bcc370a488 Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Sun, 3 May 2026 15:35:04 -0500 Subject: [PATCH] =?UTF-8?q?add=20runtime/http.el=20=E2=80=94=20HTTP=20clie?= =?UTF-8?q?nt=20and=20server=20in=20El?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thin El wrappers over seed primitives that form the public HTTP API for El programs. Covers GET/POST/DELETE, header-map variants, binary streaming to file, form-auth, v1/v2 server dispatch, and http_response envelope construction. Documents two new seed primitives needed: __http_do_map and __http_do_map_to_file (ElMap-accepting variants to avoid needing map iteration in El). --- runtime/http.el | 171 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 runtime/http.el diff --git a/runtime/http.el b/runtime/http.el new file mode 100644 index 0000000..3a5e4a4 --- /dev/null +++ b/runtime/http.el @@ -0,0 +1,171 @@ +// 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) +}