Skip to main content

netget/server/http_common/
actions.rs

1//! Shared HTTP action execution logic
2
3use crate::llm::actions::protocol_trait::ActionResult;
4use anyhow::{Context, Result};
5use serde_json::json;
6use std::collections::HashMap;
7use tracing::debug;
8
9/// Parsed HTTP response data
10#[derive(Debug)]
11pub struct HttpResponseData {
12    pub status: u16,
13    pub headers: HashMap<String, String>,
14    pub body: String,
15}
16
17/// Execute HTTP response action (shared by HTTP/1.1 and HTTP/2).
18///
19/// Deliberately lenient about the shapes models actually emit, because a
20/// rejected action is not visible to the client: `execute_actions()` only logs a
21/// warning and drops it, and the request then falls through to the default
22/// empty `200`. Concretely:
23///
24/// - `status` may be a number or a numeric string (`200` or `"200"`), and must
25///   be a real HTTP status (100-599) — a bogus value is an error here rather
26///   than a panic later while building the response.
27/// - `body` is optional: omitting it means an empty body, which is what a 204 /
28///   304 needs. A non-string body (object/array/number) is serialized to JSON
29///   text instead of being discarded.
30/// - header values that are not strings are stringified rather than dropped.
31///
32/// The result is the structured response the request handler turns into a real
33/// HTTP response; it is always UTF-8 text — there is no path for binary bodies.
34pub fn execute_http_response_action(action: serde_json::Value) -> Result<ActionResult> {
35    let status_value = action
36        .get("status")
37        .context("Missing 'status' parameter (HTTP status code, e.g. 200)")?;
38
39    let status = match status_value {
40        serde_json::Value::Number(n) => n.as_u64(),
41        // Models routinely quote the status code.
42        serde_json::Value::String(s) => s.trim().parse::<u64>().ok(),
43        _ => None,
44    }
45    .filter(|s| (100..=599).contains(s))
46    .with_context(|| {
47        format!(
48            "Invalid 'status' parameter {}: expected an HTTP status code between 100 and 599",
49            status_value
50        )
51    })? as u16;
52
53    let headers = action
54        .get("headers")
55        .and_then(|v| v.as_object())
56        .map(|obj| {
57            obj.iter()
58                .filter_map(|(k, v)| match v {
59                    serde_json::Value::Null => None,
60                    serde_json::Value::String(s) => Some((k.clone(), s.clone())),
61                    other => Some((k.clone(), other.to_string())),
62                })
63                .collect::<HashMap<String, String>>()
64        })
65        .unwrap_or_default();
66
67    let body = match action.get("body") {
68        None | Some(serde_json::Value::Null) => String::new(),
69        Some(serde_json::Value::String(s)) => s.clone(),
70        Some(other) => {
71            debug!(
72                "HTTP response body was not a string; serializing {} to JSON text",
73                other
74            );
75            other.to_string()
76        }
77    };
78
79    // Return structured data that caller will convert to HTTP response
80    let response_data = json!({
81        "status": status,
82        "headers": headers,
83        "body": body
84    });
85
86    Ok(ActionResult::Output(
87        serde_json::to_vec(&response_data).context("Failed to serialize HTTP response")?,
88    ))
89}