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
25pub(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
183pub 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
343pub(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
749pub 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 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}