#1030·plano

Malformed x-arch-state metadata can panic the prompt gateway

Author: chenshj73Created Sep 1, 2026Updated Sep 1, 2026

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:

rust
// 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:

rust
// 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():

rust
// 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:

json
{
  "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-state and 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:

rust
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.