Malformed x-arch-state metadata can panic the prompt gateway
Hi! I noticed a small robustness issue in the prompt gateway request path: a malformed client-supplied metadata["x-arch-state"] value appears to be able to panic the WASM filter instead of returning a normal 4xx error or ignoring the invalid state.
I am filing this as a reliability/input-validation issue rather than a vulnerability claim, but the input is on the request boundary for /v1/chat/completions, so it can affect gateway availability for affected requests.
Why this looks reachable
ChatCompletionsRequest accepts metadata from the request body, and the value type is a string map:
// crates/common/src/api/open_ai.rs
11 #[derive(Debug, Clone, Serialize, Deserialize)]
12 pub struct ChatCompletionsRequest {
13 #[serde(default)]
14 pub model: String,
15 pub messages: Vec<Message>,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub tools: Option<Vec<ChatCompletionTool>>,
18 #[serde(default)]
19 pub stream: bool,
20 #[serde(skip_serializing_if = "Option::is_none")]
21 pub stream_options: Option<StreamOptions>,
22 #[serde(skip_serializing_if = "Option::is_none")]
23 pub metadata: Option<HashMap<String, String>>,
24 }The internal state key is a normal string constant:
// crates/common/src/consts.rs
15 pub const CHAT_COMPLETIONS_PATH: &str = "/v1/chat/completions";
16 pub const OPENAI_RESPONSES_API_PATH: &str = "/v1/responses";
17 pub const MESSAGES_PATH: &str = "/v1/messages";
18 pub const HEALTHZ_PATH: &str = "/healthz";
19 pub const X_ARCH_STATE_HEADER: &str = "x-arch-state";
20 pub const X_ARCH_API_RESPONSE: &str = "x-arch-api-response-message";
21 pub const X_ARCH_TOOL_CALL: &str = "x-arch-tool-call-message";
22 pub const X_ARCH_FC_MODEL_RESPONSE: &str = "x-arch-fc-model-response";In on_http_request_body, the gateway checks only whether the key exists, then parses the string with unwrap():
// crates/prompt_gateway/src/http_context.rs
114 // Deserialize body into spec.
115 // Currently OpenAI API.
116 let deserialized_body: ChatCompletionsRequest = match serde_json::from_slice(&body_bytes) {
117 Ok(deserialized) => deserialized,
118 Err(e) => {
119 self.send_server_error(
120 ServerError::Deserialization(e),
121 Some(StatusCode::BAD_REQUEST),
122 );
123 return Action::Pause;
124 }
125 };
126
127 self.arch_state = match deserialized_body.metadata {
128 Some(ref metadata) => {
129 if metadata.contains_key(X_ARCH_STATE_HEADER) {
130 let arch_state_str = metadata[X_ARCH_STATE_HEADER].clone();
131 let arch_state: Vec<ArchState> = serde_json::from_str(&arch_state_str).unwrap();
132 Some(arch_state)
133 } else {
134 None
135 }
136 }
137 None => None,
138 };So a request can deserialize successfully as ChatCompletionsRequest, but still panic later if metadata["x-arch-state"] is not valid JSON for Vec<ArchState>.
Minimal example
For example, this body has valid top-level JSON and valid metadata: HashMap<String, String>, but the internal state value is malformed:
{
"model": "some-model",
"messages": [
{
"role": "user",
"content": "hello"
}
],
"metadata": {
"x-arch-state": "not-json"
}
}Expected behavior
The gateway should probably treat this like other request-body validation failures:
- return a controlled
400 Bad Request, or - ignore invalid client-provided
x-arch-stateand start with no previous Arch state.
Either behavior is better than panicking inside the request handler.
Possible fix
Replace the unwrap() with an explicit parse branch, for example:
match serde_json::from_str::<Vec<ArchState>>(&arch_state_str) {
Ok(arch_state) => Some(arch_state),
Err(e) => {
self.send_server_error(
ServerError::Deserialization(e),
Some(StatusCode::BAD_REQUEST),
);
return Action::Pause;
}
}The exact error type may need to match the project style, but the important part is to avoid unwrap() on request-derived metadata.
Source: katanemo/plano