251 lines
12 KiB
EmacsLisp
251 lines
12 KiB
EmacsLisp
// 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<String, String> 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, String>) -> 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, String>) -> 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 <token>" or "Basic <base64>").
|
||
//
|
||
// 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<String, String>, 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<String, String>, 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":<n>,"headers":<obj>,"body":"<escaped>"}
|
||
// 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<String, String>, 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__"
|
||
}
|