1use crate::console_trace;
4use crate::llm::ActionResult;
5use bytes::Bytes;
6use http_body_util::{BodyExt, Full};
7use hyper::body::Incoming;
8use hyper::{Request, Response};
9use std::collections::HashMap;
10use std::convert::Infallible;
11use tokio::sync::mpsc;
12use tracing::{debug, error, trace, warn};
13
14#[derive(Debug)]
16pub struct RequestData {
17 pub method: String,
18 pub uri: String,
19 pub version: String,
20 pub headers: HashMap<String, String>,
21 pub body_bytes: Bytes,
22}
23
24pub const MAX_REQUEST_BODY_BYTES: usize = 8 * 1024 * 1024;
33
34#[derive(Debug)]
40pub struct BodyTooLarge {
41 pub method: String,
42 pub uri: String,
43}
44
45pub async fn extract_request_data(
50 req: Request<Incoming>,
51 protocol_label: &str,
52 status_tx: &mpsc::UnboundedSender<String>,
53) -> Result<RequestData, BodyTooLarge> {
54 let method = req.method().to_string();
56 let uri = req
58 .uri()
59 .path_and_query()
60 .map(|pq| pq.as_str())
61 .unwrap_or(req.uri().path())
62 .to_string();
63 let version = format!("{:?}", req.version());
64
65 let mut headers = HashMap::new();
67 for (name, value) in req.headers() {
68 if let Ok(value_str) = value.to_str() {
69 headers.insert(name.to_string(), value_str.to_string());
70 }
71 }
72
73 let limited = http_body_util::Limited::new(req.into_body(), MAX_REQUEST_BODY_BYTES);
76 let body_bytes = match limited.collect().await {
77 Ok(collected) => collected.to_bytes(),
78 Err(e) => {
79 error!(
80 "{} {} {}: refusing request body ({}); limit is {} bytes",
81 protocol_label, method, uri, e, MAX_REQUEST_BODY_BYTES
82 );
83 let _ = status_tx.send(format!(
84 "✗ {} {} {} → 413 (request body over {} bytes)",
85 protocol_label, method, uri, MAX_REQUEST_BODY_BYTES
86 ));
87 return Err(BodyTooLarge { method, uri });
88 }
89 };
90
91 debug!(
93 "{} request (action-based): {} {} {} ({} bytes)",
94 protocol_label,
95 method,
96 uri,
97 version,
98 body_bytes.len(),
99 );
100 let _ = status_tx.send(format!(
101 "[DEBUG] {} request: {} {} {} ({} bytes)",
102 protocol_label,
103 method,
104 uri,
105 version,
106 body_bytes.len()
107 ));
108
109 trace!("{} request headers:", protocol_label);
111 for (name, value) in &headers {
112 trace!(" {}: {}", name, value);
113 let _ = status_tx.send(format!(
114 "[TRACE] {} header: {}: {}",
115 protocol_label, name, value
116 ));
117 }
118 if !body_bytes.is_empty() {
119 if let Ok(body_str) = std::str::from_utf8(&body_bytes) {
120 if let Ok(json) = serde_json::from_str::<serde_json::Value>(body_str) {
122 let pretty = serde_json::to_string_pretty(&json).unwrap_or(body_str.to_string());
123 trace!("{} request body (JSON):\n{}", protocol_label, pretty);
124 let _ = status_tx.send(format!(
125 "[TRACE] {} request body (JSON):\r\n{}",
126 protocol_label,
127 pretty.replace('\n', "\r\n")
128 ));
129 } else {
130 trace!("{} request body:\n{}", protocol_label, body_str);
131 let _ = status_tx.send(format!(
132 "[TRACE] {} request body:\r\n{}",
133 protocol_label,
134 body_str.replace('\n', "\r\n")
135 ));
136 }
137 } else {
138 console_trace!(
139 status_tx,
140 "{} request body (binary): {} bytes",
141 protocol_label,
142 body_bytes.len()
143 );
144 }
145 }
146
147 Ok(RequestData {
148 method,
149 uri,
150 version,
151 headers,
152 body_bytes,
153 })
154}
155
156pub fn build_payload_too_large_response(
161 too_large: &BodyTooLarge,
162 protocol_label: &str,
163 status_tx: &mpsc::UnboundedSender<String>,
164) -> Response<Full<Bytes>> {
165 warn!(
166 "{} {} {} decision=refused_body_too_large: over {} bytes",
167 protocol_label, too_large.method, too_large.uri, MAX_REQUEST_BODY_BYTES
168 );
169 let _ = status_tx.send(format!(
170 "→ {} {} {} → 413",
171 protocol_label, too_large.method, too_large.uri
172 ));
173 build_safe_response(
174 413,
175 [("Content-Type".to_string(), "text/plain".to_string())],
176 format!(
177 "Payload Too Large: request bodies are limited to {} bytes\n",
178 MAX_REQUEST_BODY_BYTES
179 ),
180 protocol_label,
181 )
182}
183
184pub fn build_safe_response(
197 status: u16,
198 headers: impl IntoIterator<Item = (String, String)>,
199 body: String,
200 context: &str,
201) -> Response<Full<Bytes>> {
202 let status_code = match hyper::StatusCode::from_u16(status) {
203 Ok(code) => code,
204 Err(_) => {
205 error!(
206 "{}: invalid HTTP status {} (must be 100-599), sending 500 instead",
207 context, status
208 );
209 hyper::StatusCode::INTERNAL_SERVER_ERROR
210 }
211 };
212
213 let mut builder = Response::builder().status(status_code);
214 for (name, value) in headers {
215 match (
216 hyper::header::HeaderName::from_bytes(name.as_bytes()),
217 hyper::header::HeaderValue::from_str(&value),
218 ) {
219 (Ok(n), Ok(v)) => builder = builder.header(n, v),
220 _ => warn!(
221 "{}: dropping invalid response header {:?} (name or value is not a legal HTTP header)",
222 context, name
223 ),
224 }
225 }
226
227 match builder.body(Full::new(Bytes::from(body))) {
228 Ok(response) => response,
229 Err(e) => {
230 error!(
231 "{}: failed to build response ({}), sending bare 500",
232 context, e
233 );
234 let mut fallback =
235 Response::new(Full::new(Bytes::from_static(b"Internal Server Error")));
236 *fallback.status_mut() = hyper::StatusCode::INTERNAL_SERVER_ERROR;
237 fallback
238 }
239 }
240}
241
242const MAX_RESULT_NESTING: usize = 8;
261
262fn for_each_output(results: &[ActionResult], visit: &mut impl FnMut(&[u8])) {
263 for_each_output_at(results, 0, visit)
264}
265
266fn for_each_output_at(results: &[ActionResult], depth: usize, visit: &mut impl FnMut(&[u8])) {
267 if depth >= MAX_RESULT_NESTING {
268 tracing::warn!(
271 "http_common: result nesting deeper than {MAX_RESULT_NESTING}, not descending \
272 further. An executor is producing nested Multiple results; the outputs found so \
273 far are still used."
274 );
275 return;
276 }
277 for result in results {
278 match result {
279 ActionResult::Output(data) => visit(data),
280 ActionResult::Multiple(inner) => for_each_output_at(inner, depth + 1, visit),
281 _ => {}
282 }
283 }
284}
285
286pub fn produced_http_response(results: &[ActionResult]) -> bool {
298 let mut produced = false;
299 for_each_output(results, &mut |data| {
300 if serde_json::from_slice::<serde_json::Value>(data).is_ok() {
301 produced = true;
302 }
303 });
304 produced
305}
306
307pub fn build_response(
309 protocol_results: Vec<ActionResult>,
310 protocol_label: &str,
311 method: &str,
312 uri: &str,
313 status_tx: &mpsc::UnboundedSender<String>,
314 default_response: Option<(u16, Vec<(String, String)>, String)>,
318) -> Result<Response<Full<Bytes>>, Infallible> {
319 let mut status_code = 200;
321 let mut response_headers = HashMap::new();
322 let mut response_body = String::new();
323 let mut produced_response = false;
324
325 for_each_output(&protocol_results, &mut |output_data| {
328 if let Ok(json_value) = serde_json::from_slice::<serde_json::Value>(output_data) {
330 produced_response = true;
331 if let Some(status) = json_value.get("status").and_then(|v| v.as_u64()) {
332 status_code = u16::try_from(status).unwrap_or(500);
337 }
338 if let Some(headers_obj) = json_value.get("headers").and_then(|v| v.as_object()) {
339 for (k, v) in headers_obj {
340 if let Some(v_str) = v.as_str() {
341 response_headers.insert(k.clone(), v_str.to_string());
342 }
343 }
344 }
345 if let Some(body) = json_value.get("body").and_then(|v| v.as_str()) {
346 response_body = body.to_string();
347 }
348 }
349 });
350
351 if !produced_response {
354 if let Some((status, headers, body)) = default_response {
355 status_code = status;
356 response_headers = headers.into_iter().collect();
357 response_body = body;
358 }
359 }
360
361 let _ = status_tx.send(format!(
362 "→ {} {} {} → {} ({} bytes)",
363 protocol_label,
364 method,
365 uri,
366 status_code,
367 response_body.len()
368 ));
369
370 Ok(build_safe_response(
373 status_code,
374 response_headers,
375 response_body,
376 protocol_label,
377 ))
378}
379
380pub fn build_error_response(
393 error: anyhow::Error,
394 protocol_label: &str,
395 method: &str,
396 uri: &str,
397 status_tx: &mpsc::UnboundedSender<String>,
398) -> Result<Response<Full<Bytes>>, Infallible> {
399 let failure = crate::utils::WireFailure::classify(&error);
400 let (status, decision) = match failure {
401 crate::utils::WireFailure::Overloaded => (503, "fail_closed_llm_overloaded"),
402 crate::utils::WireFailure::Unavailable => (500, "fail_closed_llm_error"),
403 };
404 error!(
405 "{} {} {} decision={} status={}: {}",
406 protocol_label, method, uri, decision, status, error
407 );
408 let _ = status_tx.send(format!(
412 "[ERROR] ✗ LLM error for {} {} → {} decision={}: {}",
413 method, uri, status, decision, error
414 ));
415
416 if status == 503 {
417 return Ok(Response::builder()
418 .status(503)
419 .header(hyper::header::RETRY_AFTER, "1")
420 .body(Full::new(Bytes::from(failure.prefixed_text())))
421 .unwrap());
422 }
423
424 Ok(Response::builder()
425 .status(500)
426 .body(Full::new(Bytes::from(failure.prefixed_text())))
427 .unwrap())
428}
429
430#[derive(Debug, Clone)]
441enum HeaderExpect {
442 Present,
444 Contains(String),
447}
448
449#[derive(Debug, Clone)]
452struct HeaderMatch {
453 name: String,
454 expect: HeaderExpect,
455}
456
457#[derive(Debug)]
460struct FilterRule {
461 methods: Option<Vec<String>>,
463 path: Option<regex::Regex>,
466 headers: Vec<HeaderMatch>,
468}
469
470impl FilterRule {
471 fn matches(&self, req: &RequestData, path: &str) -> bool {
473 if let Some(methods) = &self.methods {
474 let m = req.method.to_ascii_uppercase();
475 if !methods.iter().any(|allowed| allowed == &m) {
476 return false;
477 }
478 }
479 if let Some(re) = &self.path {
480 if !re.is_match(path) {
481 return false;
482 }
483 }
484 for hm in &self.headers {
485 let value = req
488 .headers
489 .iter()
490 .find(|(k, _)| k.eq_ignore_ascii_case(&hm.name))
491 .map(|(_, v)| v);
492 match (&hm.expect, value) {
493 (HeaderExpect::Present, Some(_)) => {}
494 (HeaderExpect::Contains(needle), Some(v))
495 if v.to_ascii_lowercase()
496 .contains(&needle.to_ascii_lowercase()) => {}
497 _ => return false,
498 }
499 }
500 true
501 }
502}
503
504#[derive(Debug, Clone)]
506struct FilteredResponse {
507 status: u16,
508 body: String,
509 headers: Vec<(String, String)>,
510}
511
512impl Default for FilteredResponse {
513 fn default() -> Self {
514 Self {
515 status: 404,
516 body: String::new(),
517 headers: Vec::new(),
518 }
519 }
520}
521
522#[derive(Debug)]
527pub struct RequestFilter {
528 rules: Vec<FilterRule>,
529 filtered_response: FilteredResponse,
530 default_response: Option<FilteredResponse>,
535 warnings: Vec<String>,
539}
540
541impl RequestFilter {
542 pub fn from_startup_params(params: Option<&serde_json::Value>) -> Self {
553 let mut rules = Vec::new();
554 let mut warnings = Vec::new();
555
556 match params.and_then(|p| p.get("request_filter")) {
557 None | Some(serde_json::Value::Null) => {}
558 Some(serde_json::Value::Array(arr)) => {
559 for (i, raw) in arr.iter().enumerate() {
560 match Self::parse_rule(raw) {
561 Ok(rule) => rules.push(rule),
562 Err(e) => {
563 let msg =
564 format!("invalid request_filter rule #{}: {} — rule ignored", i, e);
565 error!("{}", msg);
566 warnings.push(msg);
567 }
568 }
569 }
570 if !arr.is_empty() && rules.is_empty() {
571 let msg = "request_filter was configured but EVERY rule was invalid — \
572 the filter is disabled and every request will reach the LLM \
573 (fail-open)"
574 .to_string();
575 error!("{}", msg);
576 warnings.push(msg);
577 }
578 }
579 Some(other) => {
580 let msg = format!(
581 "request_filter must be an array of rule objects, got {} — \
582 filter ignored, every request will reach the LLM (fail-open)",
583 kind_of(other)
584 );
585 error!("{}", msg);
586 warnings.push(msg);
587 }
588 }
589
590 let filtered_response = params
591 .and_then(|p| p.get("filtered_response"))
592 .map(|raw| Self::parse_filtered_response(raw, &mut warnings))
593 .unwrap_or_default();
594
595 if rules.is_empty() && params.and_then(|p| p.get("filtered_response")).is_some() {
596 let msg = "filtered_response is set but no valid request_filter rules exist — \
597 it will never be used"
598 .to_string();
599 warn!("{}", msg);
600 warnings.push(msg);
601 }
602
603 let default_response = params
606 .and_then(|p| p.get("default_response"))
607 .map(|raw| Self::parse_filtered_response(raw, &mut warnings));
608
609 Self {
610 rules,
611 filtered_response,
612 default_response,
613 warnings,
614 }
615 }
616
617 pub fn default_response_parts(&self) -> Option<(u16, Vec<(String, String)>, String)> {
621 self.default_response
622 .as_ref()
623 .map(|r| (r.status, r.headers.clone(), r.body.clone()))
624 }
625
626 pub fn warnings(&self) -> &[String] {
629 &self.warnings
630 }
631
632 fn parse_rule(raw: &serde_json::Value) -> Result<FilterRule, String> {
633 let obj = raw.as_object().ok_or("rule must be an object")?;
634
635 let methods = match obj.get("methods") {
636 None | Some(serde_json::Value::Null) => None,
637 Some(serde_json::Value::Array(arr)) => Some(
638 arr.iter()
639 .filter_map(|m| m.as_str().map(|s| s.to_ascii_uppercase()))
640 .collect::<Vec<_>>(),
641 ),
642 Some(serde_json::Value::String(s)) => Some(vec![s.to_ascii_uppercase()]),
644 Some(_) => return Err("`methods` must be a string or array of strings".into()),
645 };
646
647 let path = match obj.get("path") {
648 None | Some(serde_json::Value::Null) => None,
649 Some(serde_json::Value::String(pattern)) => Some(
650 regex::Regex::new(pattern)
651 .map_err(|e| format!("invalid `path` regex {:?}: {}", pattern, e))?,
652 ),
653 Some(_) => return Err("`path` must be a regex string".into()),
654 };
655
656 let mut headers = Vec::new();
657 if let Some(hdrs) = obj.get("headers") {
658 let map = hdrs.as_object().ok_or("`headers` must be an object")?;
659 for (name, matcher) in map {
660 let expect = match matcher {
661 serde_json::Value::Bool(true) => HeaderExpect::Present,
662 serde_json::Value::Bool(false) => continue, serde_json::Value::String(s) => HeaderExpect::Contains(s.clone()),
664 _ => {
665 return Err(format!(
666 "header `{}` matcher must be true or a string",
667 name
668 ))
669 }
670 };
671 headers.push(HeaderMatch {
672 name: name.to_ascii_lowercase(),
673 expect,
674 });
675 }
676 }
677
678 if methods.is_none() && path.is_none() && headers.is_empty() {
679 debug!("request_filter rule has no conditions — matches all requests");
682 }
683
684 Ok(FilterRule {
685 methods,
686 path,
687 headers,
688 })
689 }
690
691 fn parse_filtered_response(
692 raw: &serde_json::Value,
693 warnings: &mut Vec<String>,
694 ) -> FilteredResponse {
695 let mut resp = FilteredResponse::default();
696 if let Some(status) = raw.get("status") {
697 match status.as_u64() {
698 Some(s) if (100..=599).contains(&s) => resp.status = s as u16,
701 _ => {
702 let msg = format!(
703 "filtered_response.status {} is not a valid HTTP status (100-599) — using {}",
704 status, resp.status
705 );
706 error!("{}", msg);
707 warnings.push(msg);
708 }
709 }
710 }
711 if let Some(body) = raw.get("body").and_then(|v| v.as_str()) {
712 resp.body = body.to_string();
713 }
714 if let Some(headers) = raw.get("headers").and_then(|v| v.as_object()) {
715 resp.headers = headers
716 .iter()
717 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
718 .collect();
719 }
720 resp
721 }
722
723 pub fn is_pass_through(&self) -> bool {
725 self.rules.is_empty()
726 }
727
728 pub fn allows(&self, req: &RequestData, path: &str) -> bool {
730 self.is_pass_through() || self.rules.iter().any(|r| r.matches(req, path))
731 }
732
733 pub fn rejection_parts(&self) -> (u16, Vec<(String, String)>, String) {
736 (
737 self.filtered_response.status,
738 self.filtered_response.headers.clone(),
739 self.filtered_response.body.clone(),
740 )
741 }
742
743 pub fn rejection(&self) -> Response<Full<Bytes>> {
748 build_safe_response(
749 self.filtered_response.status,
750 self.filtered_response.headers.iter().cloned(),
751 self.filtered_response.body.clone(),
752 "filtered_response",
753 )
754 }
755}
756
757fn kind_of(v: &serde_json::Value) -> &'static str {
759 match v {
760 serde_json::Value::Null => "null",
761 serde_json::Value::Bool(_) => "a boolean",
762 serde_json::Value::Number(_) => "a number",
763 serde_json::Value::String(_) => "a string",
764 serde_json::Value::Array(_) => "an array",
765 serde_json::Value::Object(_) => "an object",
766 }
767}
768
769pub fn request_handling_startup_parameters() -> Vec<crate::llm::actions::ParameterDefinition> {
772 use crate::llm::actions::ParameterDefinition;
773 vec![
774 ParameterDefinition {
775 name: "request_filter".to_string(),
776 type_hint: "array".to_string(),
777 description: "RECOMMENDED for any HTTP server a browser or the public internet will \
778 reach: set this so you do not spend a slow LLM call on favicon.ico, CORS \
779 preflights, and scanner noise. A good default that only reasons about real page \
780 loads is [{\"methods\":[\"GET\"],\"headers\":{\"accept\":\"text/html\"}}] — favicon \
781 and image probes carry `Accept: image/*`, and preflights are not GET, so they are \
782 auto-answered without a model call. \
783 Allowlist of request-match rules deciding which requests reach the LLM. \
784 A request is handled by the LLM only if it matches at least one rule; requests \
785 matching no rule get `filtered_response` (default 404) with NO LLM call. Omit this \
786 to send every request to the LLM. Each rule is an object; all present conditions \
787 must hold (AND), and rules are OR'd together. Conditions: `methods` (array of HTTP \
788 methods, case-insensitive; omit for any), `path` (a regular expression matched \
789 against the URL path, e.g. \"^/$\" or \"^/api/\"; omit for any), `headers` (object \
790 mapping header name to either true = must be present, or a string = value must \
791 contain that substring case-insensitively, e.g. {\"accept\": \"text/html\"}). \
792 Example filter that only sends real browser page loads to the LLM (favicon/OPTIONS/\
793 XHR are auto-404'd): [{\"methods\":[\"GET\"],\"headers\":{\"accept\":\"text/html\"}}]. \
794 Fail-open: a malformed rule (e.g. an invalid `path` regex) is dropped with a loud \
795 error instead of failing startup, so a typo means MORE requests reach the LLM, not \
796 fewer. Check the server status output after starting."
797 .to_string(),
798 required: false,
799 example: serde_json::json!([
800 { "methods": ["GET"], "path": "^/$", "headers": { "accept": "text/html" } },
801 { "methods": ["POST"], "path": "^/api/" }
802 ]),
803 default: None,
804 },
805 ParameterDefinition {
806 name: "filtered_response".to_string(),
807 type_hint: "object".to_string(),
808 description: "Response returned (without any LLM call) for requests that match no \
809 `request_filter` rule. Fields: `status` (number, default 404), `body` (string, \
810 default empty), `headers` (object of name→value). Ignored when no request_filter \
811 is set."
812 .to_string(),
813 required: false,
814 example: serde_json::json!({ "status": 404, "body": "Not Found" }),
815 default: None,
816 },
817 ParameterDefinition {
818 name: "default_response".to_string(),
819 type_hint: "object".to_string(),
820 description: "Response used when the model IS consulted for a request but returns no \
821 send_http_response (an empty answer). Without this, such a request gets a blank \
822 200, which looks broken. Set a sensible fallback — commonly a 404 page — so a \
823 declined request looks intentional. Same shape as filtered_response: `status` \
824 (number), `body` (string), `headers` (object of name→value)."
825 .to_string(),
826 required: false,
827 example: serde_json::json!({
828 "status": 404,
829 "headers": { "Content-Type": "text/html" },
830 "body": "<!doctype html><title>Not Found</title><h1>404</h1>"
831 }),
832 default: None,
833 },
834 ]
835}