1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub struct Http2Protocol;
16
17impl Default for Http2Protocol {
18 fn default() -> Self {
19 Self::new()
20 }
21}
22
23impl Http2Protocol {
24 pub fn new() -> Self {
25 Self
26 }
27}
28
29impl Protocol for Http2Protocol {
31 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
32 let mut params = crate::server::tls_cert_manager::get_tls_startup_parameters();
33 params.extend(crate::server::http_common::handler::request_handling_startup_parameters());
34 params.push(crate::llm::actions::ParameterDefinition {
35 name: "first_byte_timeout_secs".to_string(),
36 type_hint: "number".to_string(),
37 description: "Seconds a connected peer may send nothing at all (no TLS ClientHello, \
38 no HTTP/2 preface) before the server closes it. Default 30: HTTP/2 is \
39 client-speaks-first, and NetGet's own HTTP/2 client opens no socket \
40 until it has a request to send."
41 .to_string(),
42 required: false,
43 example: json!(30),
44 default: Some(serde_json::json!(
45 super::h2_server::FIRST_BYTE_READ_TIMEOUT.as_secs()
46 )),
47 });
48 params.push(crate::llm::actions::ParameterDefinition {
49 name: "idle_timeout_secs".to_string(),
50 type_hint: "number".to_string(),
51 description: "Seconds an established connection may carry no request before the \
52 server sends GOAWAY and closes it. Default 300, above the 90 seconds \
53 reqwest keeps an idle pooled connection. A request still being \
54 answered never counts as idle."
55 .to_string(),
56 required: false,
57 example: json!(300),
58 default: Some(serde_json::json!(
59 super::h2_server::IDLE_BETWEEN_REQUESTS_TIMEOUT.as_secs()
60 )),
61 });
62 params
63 }
64 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
65 Vec::new()
67 }
68 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
69 vec![send_http2_response_action(), push_resource_action()]
70 }
71 fn protocol_name(&self) -> &'static str {
72 "HTTP2"
73 }
74 fn get_event_types(&self) -> Vec<EventType> {
75 get_http2_event_types()
76 }
77 fn stack_name(&self) -> &'static str {
78 "ETH>IP>TCP>HTTP/2"
79 }
80 fn keywords(&self) -> Vec<&'static str> {
81 vec![
82 "http2",
83 "http/2",
84 "http 2",
85 "http2 server",
86 "http/2 server",
87 "via http2",
88 "via http/2",
89 ]
90 }
91 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
92 use crate::protocol::metadata::{
93 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
94 };
95
96 ProtocolMetadataV2::builder()
97 .state(DevelopmentState::Experimental)
98 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(443))
101 .well_known_port(443)
102 .implementation("h2 crate directly (server push), optional TLS via rustls")
103 .llm_control("Response content (status, headers, text body) + server push")
104 .e2e_testing("h2/reqwest + mocked LLM, tests/server/http2/e2e_test.rs (3 scenarios)")
105 .notes(
106 "Text bodies only, no streaming; ALPN is not negotiated, so a browser will not \
107 pick HTTP/2 over TLS on its own - clients must select h2 explicitly or use h2c",
108 )
109 .max_inbound_bytes(crate::server::http_common::MAX_REQUEST_BODY_BYTES)
110 .request_only("HTTP/2 only answers requests; every response and push rides a stream the peer opened")
111 .build()
112 }
113 fn description(&self) -> &'static str {
114 "Web server serving HTTP/2 traffic with multiplexing and header compression"
115 }
116 fn example_prompt(&self) -> &'static str {
117 "HTTP/2 server on port 8443 serving JSON API with fast multiplexed responses"
118 }
119 fn group_name(&self) -> &'static str {
120 "Core"
121 }
122
123 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
124 use crate::llm::actions::StartupExamples;
125
126 let script = r#"import json, sys
129data = json.load(sys.stdin)
130event = data["event"]
131if data["event_type_id"] == "http2_request":
132 actions = [{"type": "send_http2_response", "status": 200,
133 "headers": {"Content-Type": "application/json"},
134 "body": '{"message": "Hello from HTTP/2!"}'}]
135else:
136 actions = []
137print(json.dumps({"actions": actions}))"#;
138
139 StartupExamples::new(
140 json!({
141 "type": "open_server",
142 "port": 8443,
143 "base_stack": "http2",
144 "instruction": "HTTP/2 server with multiplexing and fast responses"
145 }),
146 json!({
147 "type": "open_server",
148 "port": 8443,
149 "base_stack": "http2",
150 "event_handlers": [{
151 "event_pattern": "http2_request",
152 "handler": {
153 "type": "script",
154 "language": "python",
155 "code": script
156 }
157 }]
158 }),
159 json!({
160 "type": "open_server",
161 "port": 8443,
162 "base_stack": "http2",
163 "event_handlers": [{
164 "event_pattern": "http2_request",
165 "handler": {
166 "type": "static",
167 "actions": [{
168 "type": "send_http2_response",
169 "status": 200,
170 "headers": {
171 "Content-Type": "application/json"
172 },
173 "body": "{\"message\": \"Hello from HTTP/2!\"}"
174 }]
175 }
176 }]
177 }),
178 )
179 }
180}
181
182impl Server for Http2Protocol {
184 fn spawn(
185 &self,
186 ctx: crate::protocol::SpawnContext,
187 ) -> std::pin::Pin<
188 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
189 > {
190 Box::pin(async move {
191 use crate::server::http2::H2Server;
192
193 let tls_config = if let Some(ref params) = ctx.startup_params {
195 match crate::server::tls_cert_manager::extract_tls_config_from_params(params) {
196 Ok(config) => config,
197 Err(e) => {
198 return Err(anyhow::anyhow!("Failed to create TLS config: {}", e));
199 }
200 }
201 } else {
202 None
203 };
204
205 let secs = |name: &str| -> anyhow::Result<Option<u64>> {
208 Ok(ctx
209 .startup_params
210 .as_ref()
211 .map(|p| p.get_optional_u64(name))
212 .transpose()?
213 .flatten())
214 };
215 let bounds = crate::server::http2::h2_server::H2Bounds::from_secs(
216 secs("first_byte_timeout_secs")?,
217 secs("idle_timeout_secs")?,
218 );
219
220 H2Server::spawn_with_push_support(
222 ctx.legacy_listen_addr(),
223 ctx.llm_client,
224 ctx.state,
225 ctx.status_tx,
226 ctx.server_id,
227 tls_config,
228 bounds,
229 )
230 .await
231 })
232 }
233 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
234 let action_type = action
235 .get("type")
236 .and_then(|v| v.as_str())
237 .context("Missing 'type' field in action")?;
238
239 match action_type {
240 "send_http2_response" => self.execute_send_http2_response(action),
241 "push_resource" => self.execute_push_resource(action),
242 _ => Err(anyhow::anyhow!("Unknown HTTP/2 action: {action_type}")),
243 }
244 }
245}
246
247impl Http2Protocol {
248 fn execute_send_http2_response(&self, action: serde_json::Value) -> Result<ActionResult> {
250 crate::server::http_common::execute_http_response_action(action)
252 }
253
254 fn execute_push_resource(&self, action: serde_json::Value) -> Result<ActionResult> {
256 use anyhow::Context;
257 use serde_json::json;
258
259 let path = action
260 .get("path")
261 .and_then(|v| v.as_str())
262 .context("Missing 'path' parameter")?;
263
264 let status = match action.get("status") {
267 None | Some(serde_json::Value::Null) => 200u16,
268 Some(v) => v
269 .as_u64()
270 .filter(|s| (100..=599).contains(s))
271 .with_context(|| {
272 format!(
273 "Invalid 'status' parameter {}: expected an HTTP status code between \
274 100 and 599",
275 v
276 )
277 })? as u16,
278 };
279
280 let headers = action
281 .get("headers")
282 .and_then(|v| v.as_object())
283 .cloned()
284 .unwrap_or_default();
285
286 let body = action.get("body").and_then(|v| v.as_str()).unwrap_or("");
287
288 let method = action
289 .get("method")
290 .and_then(|v| v.as_str())
291 .unwrap_or("GET");
292
293 let push_data = json!({
295 "_push_directive": true,
296 "path": path,
297 "method": method,
298 "status": status,
299 "headers": headers,
300 "body": body
301 });
302
303 tracing::debug!("Queued server push for {}", path);
304
305 Ok(ActionResult::Output(
306 serde_json::to_vec(&push_data).context("Failed to serialize push data")?,
307 ))
308 }
309}
310
311fn send_http2_response_action() -> ActionDefinition {
313 ActionDefinition {
314 name: "send_http2_response".to_string(),
315 description: "Respond to the HTTP/2 request that triggered this event. Emit it exactly \
316 once per request: the response is sent complete, in one piece. There is no way to \
317 stream or chunk it, and the body is sent as UTF-8 text, so binary payloads cannot be \
318 produced. ALWAYS emit exactly one send_http2_response for every request — if you \
319 emit none, the server has no answer to send and refuses the stream with 500 (or the \
320 server's configured default_response, if it has one). push_resource does not count: \
321 a PUSH_PROMISE is an extra resource offered alongside an answer, not the answer."
322 .to_string(),
323 parameters: vec![
324 Parameter {
325 name: "status".to_string(),
326 type_hint: "number".to_string(),
327 description:
328 "HTTP status code as a number between 100 and 599 (e.g. 200, 404, 500)."
329 .to_string(),
330 required: true,
331 },
332 Parameter {
333 name: "headers".to_string(),
334 type_hint: "object".to_string(),
335 description: "Optional response headers as a flat name->value object. Do not set \
336 HTTP/2 pseudo-headers (:status, :path, ...) or content-length; they are \
337 handled by the server. Illegal header names/values are dropped."
338 .to_string(),
339 required: false,
340 },
341 Parameter {
342 name: "body".to_string(),
343 type_hint: "string".to_string(),
344 description: "Response body as text. Optional: omit for an empty body (204/304). \
345 A JSON object or array is serialized to compact JSON text. Text only - bytes \
346 cannot be sent."
347 .to_string(),
348 required: false,
349 },
350 ],
351 example: json!({
352 "type": "send_http2_response",
353 "status": 200,
354 "headers": {
355 "Content-Type": "application/json"
356 },
357 "body": "{\"message\": \"Hello from HTTP/2!\"}"
358 }),
359 log_template: Some(
360 LogTemplate::new()
361 .with_info("-> HTTP/2 {status}")
362 .with_debug("HTTP/2 send_http2_response: status={status}"),
363 ),
364 }
365}
366
367fn push_resource_action() -> ActionDefinition {
369 ActionDefinition {
370 name: "push_resource".to_string(),
371 description: "Push a resource to the client proactively (HTTP/2 server push). Emit it \
372 alongside send_http2_response in the same batch; pushes are sent before the main \
373 response. Clients may refuse pushes (most modern browsers have disabled them), in \
374 which case the push is dropped and only the main response is delivered. Text bodies \
375 only."
376 .to_string(),
377 parameters: vec![
378 Parameter {
379 name: "path".to_string(),
380 type_hint: "string".to_string(),
381 description: "Resource path to push (e.g., /style.css)".to_string(),
382 required: true,
383 },
384 Parameter {
387 name: "method".to_string(),
388 type_hint: "string".to_string(),
389 description: "Method for the pushed request (default: GET). RFC 7540 §8.2 \
390 allows only safe, cacheable methods - GET or HEAD."
391 .to_string(),
392 required: false,
393 },
394 Parameter {
395 name: "status".to_string(),
396 type_hint: "number".to_string(),
397 description: "HTTP status code (default: 200)".to_string(),
398 required: false,
399 },
400 Parameter {
401 name: "headers".to_string(),
402 type_hint: "object".to_string(),
403 description: "Response headers as key-value pairs".to_string(),
404 required: false,
405 },
406 Parameter {
407 name: "body".to_string(),
408 type_hint: "string".to_string(),
409 description: "Resource content to push".to_string(),
410 required: false,
411 },
412 ],
413 example: json!({
414 "type": "push_resource",
415 "path": "/style.css",
416 "status": 200,
417 "headers": {
418 "Content-Type": "text/css"
419 },
420 "body": "body { margin: 0; }"
421 }),
422 log_template: Some(
423 LogTemplate::new()
424 .with_info("-> HTTP/2 PUSH {path}")
425 .with_debug("HTTP/2 push_resource: path={path}, status={status}"),
426 ),
427 }
428}
429
430pub static SEND_HTTP2_RESPONSE_ACTION: LazyLock<ActionDefinition> =
435 LazyLock::new(send_http2_response_action);
436pub static PUSH_RESOURCE_ACTION: LazyLock<ActionDefinition> = LazyLock::new(push_resource_action);
437
438pub static HTTP2_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
444 EventType::new(
445 "http2_request",
446 "HTTP/2 request received from client",
447 json!({"type": "placeholder", "event_id": "http2_request"}),
448 )
449 .with_parameters(vec![
450 Parameter {
451 name: "method".to_string(),
452 type_hint: "string".to_string(),
453 description: "HTTP method (GET, POST, etc.)".to_string(),
454 required: true,
455 },
456 Parameter {
457 name: "uri".to_string(),
458 type_hint: "string".to_string(),
459 description: "Request URI".to_string(),
460 required: true,
461 },
462 Parameter {
463 name: "version".to_string(),
464 type_hint: "string".to_string(),
465 description: "HTTP version (HTTP/2.0)".to_string(),
466 required: true,
467 },
468 Parameter {
469 name: "headers".to_string(),
470 type_hint: "object".to_string(),
471 description: "Request headers as key-value pairs".to_string(),
472 required: true,
473 },
474 Parameter {
475 name: "body".to_string(),
476 type_hint: "string".to_string(),
477 description:
478 "Request body decoded as UTF-8 text (empty string when there is no body). \
479 Bytes that are not valid UTF-8 are replaced with U+FFFD, so when body_is_binary \
480 is true this field is lossy and must not be treated as the exact request payload."
481 .to_string(),
482 required: false,
483 },
484 Parameter {
485 name: "body_bytes".to_string(),
486 type_hint: "number".to_string(),
487 description: "Size of the request body in bytes, before UTF-8 decoding.".to_string(),
488 required: false,
489 },
490 Parameter {
491 name: "body_is_binary".to_string(),
492 type_hint: "boolean".to_string(),
493 description: "Present and true only when the request body is not valid UTF-8. The \
494 body field is then a lossy decoding; the raw bytes are not available to you."
495 .to_string(),
496 required: false,
497 },
498 ])
499 .with_log_template(
500 LogTemplate::new()
501 .with_info("{method} {uri}")
502 .with_debug("HTTP/2 {method} {uri} v{version}")
503 .with_trace("HTTP/2: {json_pretty(.)}"),
504 )
505 .with_actions(vec![
506 SEND_HTTP2_RESPONSE_ACTION.clone(),
507 PUSH_RESOURCE_ACTION.clone(),
508 ])
509});
510
511pub fn get_http2_event_types() -> Vec<EventType> {
513 vec![HTTP2_REQUEST_EVENT.clone()]
514}