Files
2026-05-05 01:38:51 -05:00

251 lines
12 KiB
EmacsLisp
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 100599 (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__"
}