From b8f55998117a3bd8b4b72bf16a36dc244dc43675 Mon Sep 17 00:00:00 2001 From: Trezy Date: Sun, 12 Apr 2026 10:11:42 -0500 Subject: [PATCH] fix: sync plugin and host body types --- src/plugin/host/bindings.rs | 42 +++++++++++++++++----- src/plugin/host/http.rs | 69 ++++++++++++++++++++++++++++++++++++- 2 files changed, 102 insertions(+), 9 deletions(-) diff --git a/src/plugin/host/bindings.rs b/src/plugin/host/bindings.rs index 36e1303..d4e7c11 100644 --- a/src/plugin/host/bindings.rs +++ b/src/plugin/host/bindings.rs @@ -250,17 +250,31 @@ async fn host_http_request_impl( ) -> i64 { let req_bytes = match read_guest_bytes(caller, req_ptr, req_len) { Some(b) => b, - None => return 0, + None => { + tracing::error!( + "host_http_request: failed to read guest memory (ptr={req_ptr}, len={req_len})" + ); + return 0; + } }; let request: super::HttpRequest = match serde_json::from_slice(&req_bytes) { Ok(r) => r, - Err(_) => return 0, + Err(e) => { + tracing::error!("host_http_request: failed to parse request JSON: {e}"); + return 0; + } }; + let url = request.url.clone(); + let method = request.method.clone(); + let ctx = match build_host_context(caller.data()) { Some(c) => c, - None => return 0, + None => { + tracing::error!("host_http_request: failed to build host context (db missing?)"); + return 0; + } }; let result = { @@ -270,13 +284,25 @@ async fn host_http_request_impl( let response_bytes = match result { Ok(resp) => serde_json::to_vec(&serde_json::json!({"ok": resp})).unwrap_or_default(), - Err(e) => serde_json::to_vec(&serde_json::json!({ - "error": {"code": "HTTP_ERROR", "message": e.to_string(), "retryable": false} - })) - .unwrap_or_default(), + Err(e) => { + tracing::warn!("host_http_request: HTTP {method} {url} failed: {e}"); + serde_json::to_vec(&serde_json::json!({ + "error": {"code": "HTTP_ERROR", "message": e.to_string(), "retryable": false} + })) + .unwrap_or_default() + } }; - write_guest_response(caller, &response_bytes).await + let packed = write_guest_response(caller, &response_bytes).await; + if packed == 0 { + tracing::error!( + "host_http_request: write_guest_response returned 0 for {} {} (response_len={})", + method, + url, + response_bytes.len() + ); + } + packed } /// Host function: get a value from KV store diff --git a/src/plugin/host/http.rs b/src/plugin/host/http.rs index 0a1733e..1f25eff 100644 --- a/src/plugin/host/http.rs +++ b/src/plugin/host/http.rs @@ -3,18 +3,85 @@ use super::{ }; use serde::{Deserialize, Serialize}; +/// Accepts both a JSON string and a byte array for the body field, +/// so plugins can send either `"body": "text"` or `"body": [1,2,3]`. +fn deserialize_body_flexible<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: serde::Deserializer<'de>, +{ + use serde::de; + + struct BodyVisitor; + impl<'de> de::Visitor<'de> for BodyVisitor { + type Value = Option>; + + fn expecting(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + f.write_str("a string, byte array, or null") + } + + fn visit_none(self) -> Result { + Ok(None) + } + + fn visit_unit(self) -> Result { + Ok(None) + } + + fn visit_str(self, v: &str) -> Result { + Ok(Some(v.as_bytes().to_vec())) + } + + fn visit_string(self, v: String) -> Result { + Ok(Some(v.into_bytes())) + } + + fn visit_bytes(self, v: &[u8]) -> Result { + Ok(Some(v.to_vec())) + } + + fn visit_seq>(self, seq: A) -> Result { + let v: Vec = + de::Deserialize::deserialize(de::value::SeqAccessDeserializer::new(seq))?; + Ok(Some(v)) + } + + fn visit_some>( + self, + deserializer: D2, + ) -> Result { + deserializer.deserialize_any(BodyVisitor) + } + } + + deserializer.deserialize_any(BodyVisitor) +} + +/// Serialize response body as a UTF-8 string when valid, otherwise as a byte array. +/// This ensures plugins that declare `body: Option` can deserialize the response. +fn serialize_body_as_string(body: &[u8], serializer: S) -> Result +where + S: serde::Serializer, +{ + match core::str::from_utf8(body) { + Ok(s) => serializer.serialize_str(s), + Err(_) => serializer.serialize_bytes(body), + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HttpRequest { pub method: String, pub url: String, pub headers: Vec<(String, String)>, + #[serde(default, deserialize_with = "deserialize_body_flexible")] pub body: Option>, } -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct HttpResponse { pub status: u16, pub headers: Vec<(String, String)>, + #[serde(serialize_with = "serialize_body_as_string")] pub body: Vec, } -- 2.51.2