Skip to main content

harn_vm/http/
mod.rs

1use crate::value::VmDictExt;
2use std::cell::RefCell;
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use crate::stdlib::macros::harn_builtin;
7use crate::value::{VmClosure, VmError, VmValue};
8use crate::vm::Vm;
9
10mod client;
11pub(crate) mod framing;
12mod mock;
13mod sigv4;
14mod streaming;
15#[cfg(test)]
16mod tests;
17
18pub(crate) use mock::HttpMockRegistry;
19use mock::{
20    clear_http_mocks, http_mock_calls_value, parse_mock_responses, register_http_mock,
21    reset_http_mocks,
22};
23pub use mock::{http_mock_calls_snapshot, push_http_mock, HttpMockCallSnapshot, HttpMockResponse};
24
25/// Route a Harn HTTP request through the standard verb pipeline.
26///
27/// This is the entry point used by the `harness.net.*` sub-handle so
28/// every script-visible network call observes the same egress allowlist,
29/// retry policy, and mock plumbing as the legacy ambient builtins.
30pub(crate) async fn execute_http_request(
31    method: &str,
32    url: &str,
33    options: &crate::value::DictMap,
34) -> Result<VmValue, VmError> {
35    client::vm_execute_http_request(method, url, options).await
36}
37
38pub(crate) async fn execute_harness_http_request(
39    registry: &HttpMockRegistry,
40    method: &str,
41    url: &str,
42    options: &crate::value::DictMap,
43) -> Result<VmValue, VmError> {
44    client::harness_mocks::vm_execute_http_request_with_mocks(registry, method, url, options).await
45}
46
47pub(crate) fn register_harness_http_mock(
48    registry: &HttpMockRegistry,
49    method: String,
50    url_pattern: String,
51    response: &crate::value::DictMap,
52) {
53    registry.register(method, url_pattern, parse_mock_responses(response));
54}
55
56pub(crate) async fn execute_harness_http_verb(
57    registry: &HttpMockRegistry,
58    method: &str,
59    has_body: bool,
60    args: Vec<VmValue>,
61) -> Result<VmValue, VmError> {
62    client::harness_mocks::http_verb_handler(registry, method, has_body, args).await
63}
64
65pub(crate) async fn execute_harness_http_download(
66    registry: &HttpMockRegistry,
67    args: Vec<VmValue>,
68) -> Result<VmValue, VmError> {
69    client::harness_mocks::download(registry, args).await
70}
71
72pub(crate) async fn execute_harness_http_stream_open(
73    registry: &HttpMockRegistry,
74    args: Vec<VmValue>,
75) -> Result<VmValue, VmError> {
76    client::harness_mocks::stream_open(registry, args).await
77}
78
79pub(crate) async fn execute_harness_http_session_request(
80    registry: &HttpMockRegistry,
81    args: Vec<VmValue>,
82) -> Result<VmValue, VmError> {
83    client::harness_mocks::session_request(registry, args).await
84}
85
86pub(crate) fn harness_http_mock_matches(
87    registry: &HttpMockRegistry,
88    harness_method: &str,
89    args: &[VmValue],
90) -> bool {
91    let (http_method, url) = if harness_method == "request" {
92        match (args.first(), args.get(1)) {
93            (Some(VmValue::String(method)), Some(VmValue::String(url))) => {
94                (method.to_string(), url.to_string())
95            }
96            _ => return false,
97        }
98    } else if harness_method == "session_request" {
99        match (args.get(1), args.get(2)) {
100            (Some(VmValue::String(method)), Some(VmValue::String(url))) => {
101                (method.to_string(), url.to_string())
102            }
103            _ => return false,
104        }
105    } else {
106        let default_method = match harness_method {
107            "get" => "GET",
108            "post" => "POST",
109            "put" => "PUT",
110            "patch" => "PATCH",
111            "delete" => "DELETE",
112            "download" | "stream_open" => "GET",
113            _ => return false,
114        };
115        let Some(VmValue::String(url)) = args.first() else {
116            return false;
117        };
118        let options_index = match harness_method {
119            "download" => Some(2),
120            "stream_open" => Some(1),
121            _ => None,
122        };
123        let http_method = options_index
124            .and_then(|index| args.get(index))
125            .and_then(VmValue::as_dict)
126            .and_then(|options| options.get("method"))
127            .map(VmValue::display)
128            .filter(|method| !method.is_empty())
129            .unwrap_or_else(|| default_method.to_string());
130        (http_method, url.to_string())
131    };
132    registry.has_match(&http_method, &url)
133}
134#[cfg(test)]
135use mock::{mock_call_headers_value, redact_mock_call_url};
136
137#[derive(Clone)]
138struct HttpServerRoute {
139    method: String,
140    template: String,
141    handler: Arc<VmClosure>,
142    max_body_bytes: Option<usize>,
143    retain_raw_body: Option<bool>,
144}
145
146#[derive(Clone)]
147struct HttpServer {
148    routes: Vec<HttpServerRoute>,
149    before: Vec<Arc<VmClosure>>,
150    after: Vec<Arc<VmClosure>>,
151    ready: bool,
152    readiness: Option<Arc<VmClosure>>,
153    shutdown_hooks: Vec<Arc<VmClosure>>,
154    shutdown: bool,
155    max_body_bytes: usize,
156    retain_raw_body: bool,
157}
158
159pub(super) const DEFAULT_TIMEOUT_MS: u64 = 30_000;
160pub(super) const DEFAULT_BACKOFF_MS: u64 = 1_000;
161pub(super) const MAX_RETRY_DELAY_MS: u64 = 60_000;
162pub(super) const DEFAULT_RETRYABLE_STATUSES: [u16; 6] = [408, 429, 500, 502, 503, 504];
163pub(super) const DEFAULT_RETRYABLE_METHODS: [&str; 5] = ["GET", "HEAD", "PUT", "DELETE", "OPTIONS"];
164pub(super) const DEFAULT_TRANSPORT_RECEIVE_TIMEOUT_MS: u64 = 30_000;
165pub(super) const DEFAULT_MAX_STREAM_EVENTS: usize = 10_000;
166pub(super) const DEFAULT_MAX_MESSAGE_BYTES: usize = 1024 * 1024;
167pub(super) const DEFAULT_SERVER_MAX_BODY_BYTES: usize = 1024 * 1024;
168pub(super) const DEFAULT_WEBSOCKET_SERVER_IDLE_TIMEOUT_MS: u64 = 30_000;
169pub(super) const MAX_HTTP_SESSIONS: usize = 64;
170pub(super) const MAX_HTTP_STREAMS: usize = 64;
171pub(super) const MAX_SSE_STREAMS: usize = 64;
172pub(super) const MAX_SSE_SERVER_STREAMS: usize = 64;
173pub(super) const MAX_WEBSOCKETS: usize = 64;
174pub(super) const MULTIPART_MOCK_BOUNDARY: &str = "harn-boundary";
175pub(super) const MAX_HTTP_SERVERS: usize = 128;
176pub(super) const MAX_WEBSOCKET_SERVERS: usize = 16;
177
178thread_local! {
179    static TRANSPORT_HANDLE_COUNTER: RefCell<u64> = const { RefCell::new(0) };
180    static HTTP_SERVERS: RefCell<HashMap<String, HttpServer>> = RefCell::new(HashMap::new());
181}
182
183/// Reset thread-local HTTP mock state. Call between test runs.
184pub fn reset_http_state() {
185    reset_http_mocks();
186    client::reset_client_state();
187    streaming::reset_streaming_state();
188    TRANSPORT_HANDLE_COUNTER.with(|counter| *counter.borrow_mut() = 0);
189    HTTP_SERVERS.with(|servers| servers.borrow_mut().clear());
190}
191
192pub(super) fn vm_error(message: impl Into<String>) -> VmError {
193    VmError::Thrown(VmValue::String(arcstr::ArcStr::from(message.into())))
194}
195
196pub(super) fn next_transport_handle(prefix: &str) -> String {
197    TRANSPORT_HANDLE_COUNTER.with(|counter| {
198        let mut counter = counter.borrow_mut();
199        *counter += 1;
200        format!("{prefix}-{}", *counter)
201    })
202}
203
204pub(super) fn handle_from_value(value: &VmValue, builtin: &str) -> Result<String, VmError> {
205    match value {
206        VmValue::String(handle) => Ok(handle.to_string()),
207        VmValue::Dict(dict) => dict
208            .get("id")
209            .map(|id| id.display())
210            .filter(|id| !id.is_empty())
211            .ok_or_else(|| vm_error(format!("{builtin}: handle dict must contain id"))),
212        _ => Err(vm_error(format!(
213            "{builtin}: first argument must be a handle string or dict"
214        ))),
215    }
216}
217
218pub(super) fn get_options_arg(args: &[VmValue], index: usize) -> crate::value::DictMap {
219    args.get(index)
220        .and_then(|value| value.as_dict())
221        .cloned()
222        .unwrap_or_default()
223}
224
225fn dict_value(entries: crate::value::DictMap) -> VmValue {
226    VmValue::dict(entries)
227}
228
229fn get_bool_option(options: &crate::value::DictMap, key: &str, default: bool) -> bool {
230    match options.get(key) {
231        Some(VmValue::Bool(value)) => *value,
232        _ => default,
233    }
234}
235
236fn get_usize_option(
237    options: &crate::value::DictMap,
238    key: &str,
239    default: usize,
240) -> Result<usize, VmError> {
241    match options.get(key).and_then(VmValue::as_int) {
242        Some(value) if value >= 0 => Ok(value as usize),
243        Some(_) => Err(vm_error(format!("http_server: {key} must be non-negative"))),
244        None => Ok(default),
245    }
246}
247
248fn get_optional_usize_option(
249    options: &crate::value::DictMap,
250    key: &str,
251) -> Result<Option<usize>, VmError> {
252    match options.get(key).and_then(VmValue::as_int) {
253        Some(value) if value >= 0 => Ok(Some(value as usize)),
254        Some(_) => Err(vm_error(format!(
255            "http_server_route: {key} must be non-negative"
256        ))),
257        None => Ok(None),
258    }
259}
260
261fn server_from_value(value: &VmValue, builtin: &str) -> Result<String, VmError> {
262    handle_from_value(value, builtin)
263}
264
265fn closure_arg(args: &[VmValue], index: usize, builtin: &str) -> Result<Arc<VmClosure>, VmError> {
266    match args.get(index) {
267        Some(VmValue::Closure(closure)) => Ok(closure.clone()),
268        Some(other) => Err(vm_error(format!(
269            "{builtin}: argument {} must be a closure, got {}",
270            index + 1,
271            other.type_name()
272        ))),
273        None => Err(vm_error(format!(
274            "{builtin}: missing closure argument {}",
275            index + 1
276        ))),
277    }
278}
279
280fn http_server_handle_value(id: &str) -> VmValue {
281    let mut dict = crate::value::DictMap::new();
282    dict.insert(crate::value::intern_key("id"), VmValue::string(id));
283    dict.insert(
284        crate::value::intern_key("kind"),
285        VmValue::string("http_server"),
286    );
287    dict_value(dict)
288}
289
290fn header_lookup_value(headers: &crate::value::DictMap, name: &str) -> VmValue {
291    headers
292        .iter()
293        .find(|(candidate, _)| candidate.eq_ignore_ascii_case(name))
294        .map(|(_, value)| value.clone())
295        .unwrap_or(VmValue::Nil)
296}
297
298fn headers_from_value(value: &VmValue) -> crate::value::DictMap {
299    match value {
300        VmValue::Dict(dict) => dict
301            .get("headers")
302            .and_then(VmValue::as_dict)
303            .map(|headers| {
304                headers
305                    .iter()
306                    .map(|(key, value)| {
307                        (
308                            crate::value::intern_key(&key.to_ascii_lowercase()),
309                            VmValue::string(value.display()),
310                        )
311                    })
312                    .collect()
313            })
314            .unwrap_or_else(|| {
315                dict.iter()
316                    .map(|(key, value)| {
317                        (
318                            crate::value::intern_key(&key.to_ascii_lowercase()),
319                            VmValue::string(value.display()),
320                        )
321                    })
322                    .collect()
323            }),
324        _ => crate::value::DictMap::new(),
325    }
326}
327
328fn normalize_headers(value: Option<&VmValue>) -> crate::value::DictMap {
329    match value.and_then(VmValue::as_dict) {
330        Some(headers) => headers
331            .iter()
332            .map(|(key, value)| {
333                (
334                    crate::value::intern_key(&key.to_ascii_lowercase()),
335                    VmValue::string(value.display()),
336                )
337            })
338            .collect(),
339        None => crate::value::DictMap::new(),
340    }
341}
342
343/// Decode `%XX` escapes and `+` spaces entirely in byte space.
344///
345/// Also the implementation behind the `url_decode` script builtin: indexing
346/// the `&str` by escape offsets panics when `%` is followed by a multi-byte
347/// character, so every decoder routes through this one byte-based copy.
348pub(crate) fn percent_decode(input: &str) -> String {
349    let bytes = input.as_bytes();
350    let mut out = Vec::with_capacity(bytes.len());
351    let mut i = 0;
352    while i < bytes.len() {
353        if bytes[i] == b'+' {
354            out.push(b' ');
355            i += 1;
356            continue;
357        }
358        if bytes[i] == b'%' && i + 2 < bytes.len() {
359            if let (Some(hi), Some(lo)) = (hex_val(bytes[i + 1]), hex_val(bytes[i + 2])) {
360                out.push((hi << 4) | lo);
361                i += 3;
362                continue;
363            }
364        }
365        out.push(bytes[i]);
366        i += 1;
367    }
368    String::from_utf8_lossy(&out).into_owned()
369}
370
371fn hex_val(byte: u8) -> Option<u8> {
372    match byte {
373        b'0'..=b'9' => Some(byte - b'0'),
374        b'a'..=b'f' => Some(byte - b'a' + 10),
375        b'A'..=b'F' => Some(byte - b'A' + 10),
376        _ => None,
377    }
378}
379
380fn split_path_and_query(raw_path: &str) -> (String, crate::value::DictMap) {
381    let (path, query) = raw_path.split_once('?').unwrap_or((raw_path, ""));
382    let mut query_map = crate::value::DictMap::new();
383    for pair in query.split('&').filter(|part| !part.is_empty()) {
384        let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
385        query_map.insert(
386            crate::value::intern_key(&percent_decode(key)),
387            VmValue::string(percent_decode(value)),
388        );
389    }
390    (
391        if path.is_empty() { "/" } else { path }.to_string(),
392        query_map,
393    )
394}
395
396fn request_body_bytes(input: &crate::value::DictMap) -> Vec<u8> {
397    match input.get("raw_body").or_else(|| input.get("body")) {
398        Some(VmValue::Bytes(bytes)) => bytes.as_ref().clone(),
399        Some(value) => value.display().into_bytes(),
400        None => Vec::new(),
401    }
402}
403
404fn request_value(
405    method: &str,
406    path: &str,
407    path_params: crate::value::DictMap,
408    mut query: crate::value::DictMap,
409    input: &crate::value::DictMap,
410    body_bytes: &[u8],
411    retain_raw_body: bool,
412) -> VmValue {
413    if let Some(explicit_query) = input.get("query").and_then(VmValue::as_dict) {
414        query.extend(
415            explicit_query
416                .iter()
417                .map(|(key, value)| (key.clone(), value.clone())),
418        );
419    }
420
421    let headers = normalize_headers(input.get("headers"));
422    let body = String::from_utf8_lossy(body_bytes).into_owned();
423    let mut request = crate::value::DictMap::new();
424    request.insert(crate::value::intern_key("method"), VmValue::string(method));
425    request.insert(crate::value::intern_key("path"), VmValue::string(path));
426    let path_params = dict_value(path_params);
427    request.insert(crate::value::intern_key("path_params"), path_params.clone());
428    request.insert(crate::value::intern_key("params"), path_params);
429    request.insert(crate::value::intern_key("query"), dict_value(query));
430    request.insert(crate::value::intern_key("headers"), dict_value(headers));
431    request.insert(crate::value::intern_key("body"), VmValue::string(body));
432    request.insert(
433        crate::value::intern_key("raw_body"),
434        if retain_raw_body {
435            VmValue::Bytes(std::sync::Arc::new(body_bytes.to_vec()))
436        } else {
437            VmValue::Nil
438        },
439    );
440    request.insert(
441        crate::value::intern_key("body_bytes"),
442        VmValue::Int(body_bytes.len() as i64),
443    );
444    request.insert(
445        crate::value::intern_key("remote_addr"),
446        input
447            .get("remote_addr")
448            .or_else(|| input.get("remote"))
449            .map(|value| VmValue::string(value.display()))
450            .unwrap_or(VmValue::Nil),
451    );
452    request.insert(
453        crate::value::intern_key("client_ip"),
454        input
455            .get("client_ip")
456            .or_else(|| input.get("remote_ip"))
457            .or_else(|| input.get("ip"))
458            .map(|value| VmValue::string(value.display()))
459            .unwrap_or(VmValue::Nil),
460    );
461    dict_value(request)
462}
463
464fn normalize_status(status: i64) -> i64 {
465    if (100..=999).contains(&status) {
466        status
467    } else {
468        500
469    }
470}
471
472fn response_with_kind(
473    status: i64,
474    mut headers: crate::value::DictMap,
475    body: VmValue,
476    body_kind: &str,
477) -> VmValue {
478    let status = normalize_status(status);
479    let mut response = crate::value::DictMap::new();
480    if body_kind == "json" && matches!(header_lookup_value(&headers, "content-type"), VmValue::Nil)
481    {
482        headers.insert(
483            crate::value::intern_key("content-type"),
484            VmValue::string("application/json; charset=utf-8"),
485        );
486    } else if body_kind == "text"
487        && matches!(header_lookup_value(&headers, "content-type"), VmValue::Nil)
488    {
489        headers.insert(
490            crate::value::intern_key("content-type"),
491            VmValue::string("text/plain; charset=utf-8"),
492        );
493    }
494    response.insert(crate::value::intern_key("status"), VmValue::Int(status));
495    response.insert(crate::value::intern_key("headers"), dict_value(headers));
496    response.insert(
497        crate::value::intern_key("ok"),
498        VmValue::Bool((200..300).contains(&status)),
499    );
500    response.insert(
501        crate::value::intern_key("body_kind"),
502        VmValue::string(body_kind),
503    );
504    match body {
505        VmValue::Bytes(bytes) => {
506            response.insert(
507                crate::value::intern_key("body"),
508                VmValue::string(String::from_utf8_lossy(&bytes)),
509            );
510            response.insert(crate::value::intern_key("raw_body"), VmValue::Bytes(bytes));
511        }
512        other => {
513            response.insert(
514                crate::value::intern_key("body"),
515                VmValue::string(other.display()),
516            );
517            response.insert(
518                crate::value::intern_key("raw_body"),
519                VmValue::Bytes(std::sync::Arc::new(other.display().into_bytes())),
520            );
521        }
522    }
523    dict_value(response)
524}
525
526fn normalize_response(value: VmValue) -> VmValue {
527    match value {
528        VmValue::Dict(dict) if dict.contains_key("status") => {
529            let status = dict.get("status").and_then(VmValue::as_int).unwrap_or(200);
530            let headers = dict
531                .get("headers")
532                .and_then(VmValue::as_dict)
533                .cloned()
534                .unwrap_or_default();
535            let body_kind = dict
536                .get("body_kind")
537                .or_else(|| dict.get("kind"))
538                .map(|value| value.display())
539                .unwrap_or_else(|| "text".to_string());
540            let body = dict
541                .get("raw_body")
542                .filter(|value| matches!(value, VmValue::Bytes(_)))
543                .or_else(|| dict.get("body"))
544                .cloned()
545                .unwrap_or(VmValue::Nil);
546            response_with_kind(status, headers, body, &body_kind)
547        }
548        VmValue::Nil => response_with_kind(204, crate::value::DictMap::new(), VmValue::Nil, "text"),
549        other => response_with_kind(200, crate::value::DictMap::new(), other, "text"),
550    }
551}
552
553fn body_limit_response(limit: usize, actual: usize) -> VmValue {
554    let mut headers = crate::value::DictMap::new();
555    headers.insert(
556        crate::value::intern_key("content-type"),
557        VmValue::string("text/plain; charset=utf-8"),
558    );
559    headers.insert(
560        crate::value::intern_key("connection"),
561        VmValue::string("close"),
562    );
563    headers.insert(
564        crate::value::intern_key("x-harn-body-limit"),
565        VmValue::string(limit.to_string()),
566    );
567    response_with_kind(
568        413,
569        headers,
570        VmValue::string(format!("request body too large: {actual} > {limit} bytes")),
571        "text",
572    )
573}
574
575fn not_found_response(method: &str, path: &str) -> VmValue {
576    response_with_kind(
577        404,
578        crate::value::DictMap::new(),
579        VmValue::string(format!("no route for {method} {path}")),
580        "text",
581    )
582}
583
584fn unavailable_response(message: &str) -> VmValue {
585    response_with_kind(
586        503,
587        crate::value::DictMap::new(),
588        VmValue::string(message),
589        "text",
590    )
591}
592
593#[expect(
594    clippy::string_slice,
595    reason = "slices cut at ASCII `{`/`}`/`:` delimiters verified by starts_with/ends_with"
596)]
597fn route_template_match(template: &str, path: &str) -> Option<crate::value::DictMap> {
598    let template_segments: Vec<&str> = template.trim_matches('/').split('/').collect();
599    let path_segments: Vec<&str> = path.trim_matches('/').split('/').collect();
600    if template == "/" && path == "/" {
601        return Some(crate::value::DictMap::new());
602    }
603    if template_segments.len() != path_segments.len() {
604        return None;
605    }
606    let mut params = crate::value::DictMap::new();
607    for (tmpl, actual) in template_segments.iter().zip(path_segments.iter()) {
608        if tmpl.starts_with('{') && tmpl.ends_with('}') && tmpl.len() > 2 {
609            params.insert(
610                crate::value::intern_key(&tmpl[1..tmpl.len() - 1]),
611                VmValue::string(percent_decode(actual)),
612            );
613        } else if tmpl.starts_with(':') && tmpl.len() > 1 {
614            params.insert(
615                crate::value::intern_key(&tmpl[1..]),
616                VmValue::string(percent_decode(actual)),
617            );
618        } else if tmpl != actual {
619            return None;
620        }
621    }
622    Some(params)
623}
624
625fn matching_route(
626    server: &HttpServer,
627    method: &str,
628    path: &str,
629) -> Option<(HttpServerRoute, crate::value::DictMap)> {
630    server.routes.iter().find_map(|route| {
631        if route.method != "*" && !route.method.eq_ignore_ascii_case(method) {
632            return None;
633        }
634        route_template_match(&route.template, path).map(|params| (route.clone(), params))
635    })
636}
637
638async fn call_server_closure(
639    ctx: &crate::vm::AsyncBuiltinCtx,
640    closure: &Arc<VmClosure>,
641    args: &[VmValue],
642    _builtin: &str,
643) -> Result<VmValue, VmError> {
644    let mut vm = ctx.child_vm();
645    let result = vm.call_closure_pub(closure, args).await;
646    ctx.forward_output(&vm.take_output());
647    result
648}
649
650fn value_is_response(value: &VmValue) -> bool {
651    matches!(value, VmValue::Dict(dict) if dict.contains_key("status"))
652}
653
654async fn run_http_server_request(
655    ctx: &crate::vm::AsyncBuiltinCtx,
656    server_id: &str,
657    request: VmValue,
658) -> Result<VmValue, VmError> {
659    let server = HTTP_SERVERS.with(|servers| servers.borrow().get(server_id).cloned());
660    let Some(server) = server else {
661        return Err(vm_error(format!(
662            "http_server_request: unknown server handle '{server_id}'"
663        )));
664    };
665    if server.shutdown {
666        return Ok(unavailable_response("server is shut down"));
667    }
668    if !server.ready {
669        return Ok(unavailable_response("server is not ready"));
670    }
671    if let Some(readiness) = &server.readiness {
672        let ready = call_server_closure(
673            ctx,
674            readiness,
675            &[http_server_handle_value(server_id)],
676            "http_server_request",
677        )
678        .await?;
679        if !ready.is_truthy() {
680            return Ok(unavailable_response("server is not ready"));
681        }
682    }
683
684    let input = request.as_dict().cloned().unwrap_or_default();
685    let method = input
686        .get("method")
687        .map(|value| value.display())
688        .filter(|value| !value.is_empty())
689        .unwrap_or_else(|| "GET".to_string())
690        .to_ascii_uppercase();
691    let raw_path = input
692        .get("path")
693        .map(|value| value.display())
694        .filter(|value| !value.is_empty())
695        .unwrap_or_else(|| "/".to_string());
696    let (path, query) = split_path_and_query(&raw_path);
697    let body_bytes = request_body_bytes(&input);
698
699    let Some((route, path_params)) = matching_route(&server, &method, &path) else {
700        return Ok(not_found_response(&method, &path));
701    };
702
703    let limit = route.max_body_bytes.unwrap_or(server.max_body_bytes);
704    if body_bytes.len() > limit {
705        return Ok(body_limit_response(limit, body_bytes.len()));
706    }
707    let retain_raw_body = route.retain_raw_body.unwrap_or(server.retain_raw_body);
708    let mut req = request_value(
709        &method,
710        &path,
711        path_params,
712        query,
713        &input,
714        &body_bytes,
715        retain_raw_body,
716    );
717
718    for before in &server.before {
719        let result =
720            call_server_closure(ctx, before, &[req.clone()], "http_server_request").await?;
721        if value_is_response(&result) {
722            return Ok(normalize_response(result));
723        }
724        if !matches!(result, VmValue::Nil) {
725            req = result;
726        }
727    }
728
729    let handler_result =
730        call_server_closure(ctx, &route.handler, &[req.clone()], "http_server_request").await?;
731    let mut response = normalize_response(handler_result);
732
733    for after in &server.after {
734        let result = call_server_closure(
735            ctx,
736            after,
737            &[response.clone(), req.clone()],
738            "http_server_request",
739        )
740        .await?;
741        if !matches!(result, VmValue::Nil) {
742            response = normalize_response(result);
743        }
744    }
745
746    Ok(response)
747}
748
749/// Register HTTP builtins on a VM.
750pub fn register_http_builtins(vm: &mut Vm) {
751    register_http_tls_builtins(vm);
752    register_http_server_builtins(vm);
753    register_http_mock_builtins(vm);
754    client::register_http_client_builtins(vm);
755    streaming::register_http_streaming_builtins(vm);
756}
757
758fn register_http_tls_builtins(vm: &mut Vm) {
759    vm.register_builtin("__http_server_tls_plain", |_args, _out| {
760        Ok(http_server_tls_config_value(
761            "plain",
762            false,
763            "http",
764            false,
765            crate::value::DictMap::new(),
766        ))
767    });
768    vm.register_builtin("__http_server_tls_edge", |args, _out| {
769        let options = get_options_arg(args, 0);
770        Ok(http_server_tls_config_value(
771            "edge",
772            false,
773            "https",
774            vm_get_bool_option(&options, "hsts", true),
775            hsts_options(&options),
776        ))
777    });
778    vm.register_builtin("__http_server_tls_pem", |args, _out| {
779        if args.len() < 2 {
780            return Err(vm_error(
781                "http_server_tls_pem: requires cert path and key path",
782            ));
783        }
784        let cert_path = args[0].display();
785        let key_path = args[1].display();
786        if !std::path::Path::new(&cert_path).is_file() {
787            return Err(vm_error(format!(
788                "http_server_tls_pem: certificate not found: {cert_path}"
789            )));
790        }
791        if !std::path::Path::new(&key_path).is_file() {
792            return Err(vm_error(format!(
793                "http_server_tls_pem: private key not found: {key_path}"
794            )));
795        }
796        let mut extra = crate::value::DictMap::new();
797        extra.put_str("cert_path", cert_path);
798        extra.put_str("key_path", key_path);
799        Ok(http_server_tls_config_value(
800            "pem", true, "https", true, extra,
801        ))
802    });
803    vm.register_builtin("__http_server_tls_self_signed_dev", |args, _out| {
804        let hosts = tls_hosts_arg(args.first())?;
805        let cert = rcgen::generate_simple_self_signed(hosts.clone()).map_err(|error| {
806            vm_error(format!(
807                "http_server_tls_self_signed_dev: failed to generate certificate: {error}"
808            ))
809        })?;
810        let mut extra = crate::value::DictMap::new();
811        extra.insert(
812            crate::value::intern_key("hosts"),
813            VmValue::List(std::sync::Arc::new(
814                hosts
815                    .into_iter()
816                    .map(|host| VmValue::String(arcstr::ArcStr::from(host)))
817                    .collect(),
818            )),
819        );
820        extra.put_str("cert_pem", cert.cert.pem());
821        extra.put_str("key_pem", cert.signing_key.serialize_pem());
822        Ok(http_server_tls_config_value(
823            "self_signed_dev",
824            true,
825            "https",
826            false,
827            extra,
828        ))
829    });
830    vm.register_builtin("__http_server_security_headers", |args, _out| {
831        let Some(VmValue::Dict(config)) = args.first() else {
832            return Err(vm_error(
833                "http_server_security_headers: requires a TLS config dict",
834            ));
835        };
836        Ok(VmValue::dict(http_server_security_headers(config)))
837    });
838}
839
840fn register_http_server_builtins(vm: &mut Vm) {
841    // --- Inbound HTTP server primitives ---
842
843    vm.register_builtin("__http_server", |args, _out| {
844        let options = get_options_arg(args, 0);
845        let server = HttpServer {
846            routes: Vec::new(),
847            before: Vec::new(),
848            after: Vec::new(),
849            ready: get_bool_option(&options, "ready", true),
850            readiness: None,
851            shutdown_hooks: Vec::new(),
852            shutdown: false,
853            max_body_bytes: get_usize_option(
854                &options,
855                "max_body_bytes",
856                DEFAULT_SERVER_MAX_BODY_BYTES,
857            )?,
858            retain_raw_body: get_bool_option(&options, "retain_raw_body", true),
859        };
860        let id = next_transport_handle("http-server");
861        HTTP_SERVERS.with(|servers| {
862            let mut servers = servers.borrow_mut();
863            if servers.len() >= MAX_HTTP_SERVERS {
864                return Err(vm_error(format!(
865                    "http_server: maximum open servers ({MAX_HTTP_SERVERS}) reached"
866                )));
867            }
868            servers.insert(id.clone(), server);
869            Ok(())
870        })?;
871        Ok(http_server_handle_value(&id))
872    });
873
874    vm.register_builtin("__http_server_route", |args, _out| {
875        if args.len() < 4 {
876            return Err(vm_error(
877                "http_server_route: requires server, method, path template, and handler",
878            ));
879        }
880        let server_id = server_from_value(&args[0], "http_server_route")?;
881        let method = args[1].display().to_ascii_uppercase();
882        if method.is_empty() {
883            return Err(vm_error("http_server_route: method is required"));
884        }
885        let template = args[2].display();
886        if !template.starts_with('/') {
887            return Err(vm_error(
888                "http_server_route: path template must start with '/'",
889            ));
890        }
891        let handler = closure_arg(args, 3, "http_server_route")?;
892        let options = get_options_arg(args, 4);
893        let route = HttpServerRoute {
894            method,
895            template,
896            handler,
897            max_body_bytes: get_optional_usize_option(&options, "max_body_bytes")?,
898            retain_raw_body: match options.get("retain_raw_body") {
899                Some(VmValue::Bool(value)) => Some(*value),
900                _ => None,
901            },
902        };
903        HTTP_SERVERS.with(|servers| {
904            let mut servers = servers.borrow_mut();
905            let server = servers.get_mut(&server_id).ok_or_else(|| {
906                vm_error(format!("http_server_route: unknown server '{server_id}'"))
907            })?;
908            server.routes.push(route);
909            Ok::<_, VmError>(())
910        })?;
911        Ok(http_server_handle_value(&server_id))
912    });
913
914    vm.register_builtin("__http_server_before", |args, _out| {
915        if args.len() < 2 {
916            return Err(vm_error("http_server_before: requires server and handler"));
917        }
918        let server_id = server_from_value(&args[0], "http_server_before")?;
919        let handler = closure_arg(args, 1, "http_server_before")?;
920        HTTP_SERVERS.with(|servers| {
921            let mut servers = servers.borrow_mut();
922            let server = servers.get_mut(&server_id).ok_or_else(|| {
923                vm_error(format!("http_server_before: unknown server '{server_id}'"))
924            })?;
925            server.before.push(handler);
926            Ok::<_, VmError>(())
927        })?;
928        Ok(http_server_handle_value(&server_id))
929    });
930
931    vm.register_builtin("__http_server_after", |args, _out| {
932        if args.len() < 2 {
933            return Err(vm_error("http_server_after: requires server and handler"));
934        }
935        let server_id = server_from_value(&args[0], "http_server_after")?;
936        let handler = closure_arg(args, 1, "http_server_after")?;
937        HTTP_SERVERS.with(|servers| {
938            let mut servers = servers.borrow_mut();
939            let server = servers.get_mut(&server_id).ok_or_else(|| {
940                vm_error(format!("http_server_after: unknown server '{server_id}'"))
941            })?;
942            server.after.push(handler);
943            Ok::<_, VmError>(())
944        })?;
945        Ok(http_server_handle_value(&server_id))
946    });
947
948    vm.register_async_builtin("__http_server_request", |ctx, args| async move {
949        if args.len() < 2 {
950            return Err(vm_error("http_server_request: requires server and request"));
951        }
952        let server_id = server_from_value(&args[0], "http_server_request")?;
953        run_http_server_request(&ctx, &server_id, args[1].clone()).await
954    });
955
956    vm.register_async_builtin("__http_server_test", |ctx, args| async move {
957        if args.len() < 2 {
958            return Err(vm_error("http_server_test: requires server and request"));
959        }
960        let server_id = server_from_value(&args[0], "http_server_test")?;
961        run_http_server_request(&ctx, &server_id, args[1].clone()).await
962    });
963
964    vm.register_builtin("__http_server_set_ready", |args, _out| {
965        if args.len() < 2 {
966            return Err(vm_error(
967                "http_server_set_ready: requires server and ready bool",
968            ));
969        }
970        let server_id = server_from_value(&args[0], "http_server_set_ready")?;
971        let ready = matches!(args[1], VmValue::Bool(true));
972        HTTP_SERVERS.with(|servers| {
973            let mut servers = servers.borrow_mut();
974            let server = servers.get_mut(&server_id).ok_or_else(|| {
975                vm_error(format!(
976                    "http_server_set_ready: unknown server '{server_id}'"
977                ))
978            })?;
979            server.ready = ready;
980            Ok::<_, VmError>(())
981        })?;
982        Ok(VmValue::Bool(ready))
983    });
984
985    vm.register_builtin("__http_server_readiness", |args, _out| {
986        if args.len() < 2 {
987            return Err(vm_error(
988                "http_server_readiness: requires server and readiness closure",
989            ));
990        }
991        let server_id = server_from_value(&args[0], "http_server_readiness")?;
992        let handler = closure_arg(args, 1, "http_server_readiness")?;
993        HTTP_SERVERS.with(|servers| {
994            let mut servers = servers.borrow_mut();
995            let server = servers.get_mut(&server_id).ok_or_else(|| {
996                vm_error(format!(
997                    "http_server_readiness: unknown server '{server_id}'"
998                ))
999            })?;
1000            server.readiness = Some(handler);
1001            Ok::<_, VmError>(())
1002        })?;
1003        Ok(http_server_handle_value(&server_id))
1004    });
1005
1006    vm.register_async_builtin("__http_server_ready", |ctx, args| async move {
1007        let Some(server_arg) = args.first() else {
1008            return Err(vm_error("http_server_ready: requires server"));
1009        };
1010        let server_id = server_from_value(server_arg, "http_server_ready")?;
1011        let server = HTTP_SERVERS.with(|servers| servers.borrow().get(&server_id).cloned());
1012        let Some(server) = server else {
1013            return Err(vm_error(format!(
1014                "http_server_ready: unknown server '{server_id}'"
1015            )));
1016        };
1017        if server.shutdown {
1018            return Ok(VmValue::Bool(false));
1019        }
1020        let Some(readiness) = server.readiness else {
1021            return Ok(VmValue::Bool(server.ready));
1022        };
1023        let result = call_server_closure(
1024            &ctx,
1025            &readiness,
1026            &[http_server_handle_value(&server_id)],
1027            "http_server_ready",
1028        )
1029        .await?;
1030        Ok(VmValue::Bool(result.is_truthy()))
1031    });
1032
1033    vm.register_builtin("__http_server_on_shutdown", |args, _out| {
1034        if args.len() < 2 {
1035            return Err(vm_error(
1036                "http_server_on_shutdown: requires server and handler",
1037            ));
1038        }
1039        let server_id = server_from_value(&args[0], "http_server_on_shutdown")?;
1040        let handler = closure_arg(args, 1, "http_server_on_shutdown")?;
1041        HTTP_SERVERS.with(|servers| {
1042            let mut servers = servers.borrow_mut();
1043            let server = servers.get_mut(&server_id).ok_or_else(|| {
1044                vm_error(format!(
1045                    "http_server_on_shutdown: unknown server '{server_id}'"
1046                ))
1047            })?;
1048            server.shutdown_hooks.push(handler);
1049            Ok::<_, VmError>(())
1050        })?;
1051        Ok(http_server_handle_value(&server_id))
1052    });
1053
1054    vm.register_async_builtin("__http_server_shutdown", |ctx, args| async move {
1055        let Some(server_arg) = args.first() else {
1056            return Err(vm_error("http_server_shutdown: requires server"));
1057        };
1058        let server_id = server_from_value(server_arg, "http_server_shutdown")?;
1059        let hooks = HTTP_SERVERS.with(|servers| {
1060            let mut servers = servers.borrow_mut();
1061            let server = servers.get_mut(&server_id).ok_or_else(|| {
1062                vm_error(format!(
1063                    "http_server_shutdown: unknown server '{server_id}'"
1064                ))
1065            })?;
1066            server.shutdown = true;
1067            Ok::<_, VmError>(server.shutdown_hooks.clone())
1068        })?;
1069        for hook in hooks {
1070            let _ = call_server_closure(
1071                &ctx,
1072                &hook,
1073                &[http_server_handle_value(&server_id)],
1074                "http_server_shutdown",
1075            )
1076            .await?;
1077        }
1078        Ok(VmValue::Bool(true))
1079    });
1080
1081    vm.register_builtin_def(&HTTP_RESPONSE_IMPL_DEF);
1082    vm.register_builtin_def(&HTTP_RESPONSE_TEXT_IMPL_DEF);
1083    vm.register_builtin_def(&HTTP_RESPONSE_JSON_IMPL_DEF);
1084    vm.register_builtin_def(&HTTP_RESPONSE_BYTES_IMPL_DEF);
1085    vm.register_builtin_def(&HTTP_HEADER_IMPL_DEF);
1086}
1087
1088#[harn_builtin(exposure = "pure", effects = [], sig = "http_response(status?: int, body?: any, headers?: dict) -> dict", category = "http")]
1089fn http_response_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1090    let status = args.first().and_then(VmValue::as_int).unwrap_or(200);
1091    let body = args.get(1).cloned().unwrap_or(VmValue::Nil);
1092    let headers = args
1093        .get(2)
1094        .and_then(VmValue::as_dict)
1095        .cloned()
1096        .unwrap_or_default();
1097    Ok(response_with_kind(status, headers, body, "text"))
1098}
1099
1100#[harn_builtin(exposure = "pure", effects = [], sig = "http_response_text(body?: any, options?: dict) -> dict", category = "http")]
1101fn http_response_text_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1102    let body = args.first().cloned().unwrap_or(VmValue::Nil);
1103    let options = get_options_arg(args, 1);
1104    let status = options
1105        .get("status")
1106        .and_then(VmValue::as_int)
1107        .unwrap_or(200);
1108    let headers = options
1109        .get("headers")
1110        .and_then(VmValue::as_dict)
1111        .cloned()
1112        .unwrap_or_default();
1113    Ok(response_with_kind(status, headers, body, "text"))
1114}
1115
1116#[harn_builtin(exposure = "pure", effects = [], sig = "http_response_json(body?: any, options?: dict) -> dict", category = "http")]
1117fn http_response_json_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1118    let body = args
1119        .first()
1120        .map(crate::stdlib::json::vm_value_to_json)
1121        .map(VmValue::string)
1122        .unwrap_or_else(|| VmValue::string("null"));
1123    let options = get_options_arg(args, 1);
1124    let status = options
1125        .get("status")
1126        .and_then(VmValue::as_int)
1127        .unwrap_or(200);
1128    let headers = options
1129        .get("headers")
1130        .and_then(VmValue::as_dict)
1131        .cloned()
1132        .unwrap_or_default();
1133    Ok(response_with_kind(status, headers, body, "json"))
1134}
1135
1136#[harn_builtin(exposure = "pure", effects = [], sig = "http_response_bytes(body?: any, options?: dict) -> dict", category = "http")]
1137fn http_response_bytes_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1138    let body = match args.first() {
1139        Some(VmValue::Bytes(bytes)) => VmValue::Bytes(bytes.clone()),
1140        Some(value) => VmValue::Bytes(std::sync::Arc::new(value.display().into_bytes())),
1141        None => VmValue::Bytes(std::sync::Arc::new(Vec::new())),
1142    };
1143    let options = get_options_arg(args, 1);
1144    let status = options
1145        .get("status")
1146        .and_then(VmValue::as_int)
1147        .unwrap_or(200);
1148    let headers = options
1149        .get("headers")
1150        .and_then(VmValue::as_dict)
1151        .cloned()
1152        .unwrap_or_default();
1153    Ok(response_with_kind(status, headers, body, "bytes"))
1154}
1155
1156#[harn_builtin(exposure = "pure", effects = [], sig = "http_header(source: dict | list, name: string) -> string?", category = "http")]
1157fn http_header_impl(args: &[VmValue], _out: &mut String) -> Result<VmValue, VmError> {
1158    if args.len() < 2 {
1159        return Err(vm_error(
1160            "http_header: requires headers/request/response and name",
1161        ));
1162    }
1163    Ok(header_lookup_value(
1164        &headers_from_value(&args[0]),
1165        &args[1].display(),
1166    ))
1167}
1168
1169fn register_http_mock_builtins(vm: &mut Vm) {
1170    vm.register_capability_method(
1171        harn_builtin_meta::CapabilityId::Testing,
1172        "http_mock",
1173        |args, _out| {
1174            let method = args.first().map(VmValue::display).unwrap_or_default();
1175            let url_pattern = args.get(1).map(VmValue::display).unwrap_or_default();
1176            let response = args
1177                .get(2)
1178                .and_then(VmValue::as_dict)
1179                .cloned()
1180                .unwrap_or_default();
1181            register_http_mock(method, url_pattern, parse_mock_responses(&response));
1182            Ok(VmValue::Nil)
1183        },
1184    );
1185    vm.register_capability_method(
1186        harn_builtin_meta::CapabilityId::Testing,
1187        "http_mock_clear",
1188        |_args, _out| {
1189            clear_http_mocks();
1190            client::clear_http_streams();
1191            Ok(VmValue::Nil)
1192        },
1193    );
1194    vm.register_capability_method(
1195        harn_builtin_meta::CapabilityId::Testing,
1196        "http_mock_calls",
1197        |args, _out| {
1198            let options = get_options_arg(args, 0);
1199            let include_sensitive = get_bool_option(&options, "include_sensitive", false)
1200                || get_bool_option(&options, "include_sensitive_headers", false);
1201            let redact_sensitive = get_bool_option(
1202                &options,
1203                "redact_sensitive",
1204                get_bool_option(&options, "redact_headers", true),
1205            ) && !include_sensitive;
1206            Ok(VmValue::List(std::sync::Arc::new(http_mock_calls_value(
1207                redact_sensitive,
1208            ))))
1209        },
1210    );
1211}
1212
1213fn http_server_tls_config_value(
1214    mode: &str,
1215    terminate_tls: bool,
1216    scheme: &str,
1217    hsts: bool,
1218    extra: crate::value::DictMap,
1219) -> VmValue {
1220    let mut dict = crate::value::DictMap::new();
1221    dict.put_str("mode", mode);
1222    dict.insert(
1223        crate::value::intern_key("terminate_tls"),
1224        VmValue::Bool(terminate_tls),
1225    );
1226    dict.put_str("scheme", scheme);
1227    dict.insert(crate::value::intern_key("hsts"), VmValue::Bool(hsts));
1228    for (key, value) in extra {
1229        dict.insert(key, value);
1230    }
1231    VmValue::dict(dict)
1232}
1233
1234fn hsts_options(options: &crate::value::DictMap) -> crate::value::DictMap {
1235    let mut hsts = crate::value::DictMap::new();
1236    hsts.insert(
1237        crate::value::intern_key("hsts_max_age_seconds"),
1238        VmValue::Int(vm_get_int_option(
1239            options,
1240            "hsts_max_age_seconds",
1241            31_536_000,
1242        )),
1243    );
1244    hsts.insert(
1245        crate::value::intern_key("hsts_include_subdomains"),
1246        VmValue::Bool(vm_get_bool_option(
1247            options,
1248            "hsts_include_subdomains",
1249            false,
1250        )),
1251    );
1252    hsts.insert(
1253        crate::value::intern_key("hsts_preload"),
1254        VmValue::Bool(vm_get_bool_option(options, "hsts_preload", false)),
1255    );
1256    hsts
1257}
1258
1259fn http_server_security_headers(config: &crate::value::DictMap) -> crate::value::DictMap {
1260    let hsts_enabled = vm_get_bool_option(config, "hsts", false);
1261    if !hsts_enabled {
1262        return crate::value::DictMap::new();
1263    }
1264    let mut value = format!(
1265        "max-age={}",
1266        vm_get_int_option(config, "hsts_max_age_seconds", 31_536_000).max(0)
1267    );
1268    if vm_get_bool_option(config, "hsts_include_subdomains", false) {
1269        value.push_str("; includeSubDomains");
1270    }
1271    if vm_get_bool_option(config, "hsts_preload", false) {
1272        value.push_str("; preload");
1273    }
1274    crate::value::DictMap::from_iter([(
1275        crate::value::intern_key("strict-transport-security"),
1276        VmValue::String(arcstr::ArcStr::from(value)),
1277    )])
1278}
1279
1280fn tls_hosts_arg(value: Option<&VmValue>) -> Result<Vec<String>, VmError> {
1281    match value {
1282        None | Some(VmValue::Nil) => Ok(vec!["localhost".to_string(), "127.0.0.1".to_string()]),
1283        Some(VmValue::List(hosts)) => {
1284            let mut parsed = Vec::new();
1285            for host in hosts.iter() {
1286                let host = host.display();
1287                if host.is_empty() {
1288                    return Err(vm_error(
1289                        "http_server_tls_self_signed_dev: host names must be non-empty",
1290                    ));
1291                }
1292                parsed.push(host);
1293            }
1294            if parsed.is_empty() {
1295                return Err(vm_error(
1296                    "http_server_tls_self_signed_dev: host list must not be empty",
1297                ));
1298            }
1299            Ok(parsed)
1300        }
1301        Some(other) => {
1302            let host = other.display();
1303            if host.is_empty() {
1304                return Err(vm_error(
1305                    "http_server_tls_self_signed_dev: host name must be non-empty",
1306                ));
1307            }
1308            Ok(vec![host])
1309        }
1310    }
1311}
1312
1313pub(super) fn vm_get_int_option(options: &crate::value::DictMap, key: &str, default: i64) -> i64 {
1314    options.get(key).and_then(|v| v.as_int()).unwrap_or(default)
1315}
1316
1317pub(super) fn vm_get_bool_option(
1318    options: &crate::value::DictMap,
1319    key: &str,
1320    default: bool,
1321) -> bool {
1322    match options.get(key) {
1323        Some(VmValue::Bool(b)) => *b,
1324        _ => default,
1325    }
1326}
1327
1328pub(super) fn vm_get_int_option_prefer(
1329    options: &crate::value::DictMap,
1330    canonical: &str,
1331    alias: &str,
1332    default: i64,
1333) -> i64 {
1334    options
1335        .get(canonical)
1336        .and_then(|value| value.as_int())
1337        .or_else(|| options.get(alias).and_then(|value| value.as_int()))
1338        .unwrap_or(default)
1339}
1340
1341pub(super) fn vm_get_optional_int_option(
1342    options: &crate::value::DictMap,
1343    key: &str,
1344) -> Option<u64> {
1345    options
1346        .get(key)
1347        .and_then(|value| value.as_int())
1348        .map(|value| value.max(0) as u64)
1349}
1350
1351pub(super) fn string_option(options: &crate::value::DictMap, key: &str) -> Option<String> {
1352    options
1353        .get(key)
1354        .map(|value| value.display())
1355        .filter(|value| !value.is_empty())
1356}