{"record":{"id":"1fd6ddee367ea75d","repo":"clockworklabs/SpacetimeDB","slug":"invalid-header-name-from-host","errorCode":null,"errorMessage":"Invalid header name from host","messagePattern":"Invalid header name from host","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/bindings/src/http.rs","lineNumber":653,"sourceCode":"\n    let request = http::Request::builder()\n        .method(method)\n        .uri(http::Uri::from_str(&uri).expect(\"Invalid URI from host\"))\n        .body(Body::from_bytes(body))\n        .expect(\"Failed to build request\");\n\n    let (mut parts, body) = request.into_parts();\n    parts.version = match version {\n        st_http::Version::Http09 => http::Version::HTTP_09,\n        st_http::Version::Http10 => http::Version::HTTP_10,\n        st_http::Version::Http11 => http::Version::HTTP_11,\n        st_http::Version::Http2 => http::Version::HTTP_2,\n        st_http::Version::Http3 => http::Version::HTTP_3,\n    };\n    parts.headers = headers\n        .into_iter()\n        .map(|(k, v)| {\n            let name = http::HeaderName::from_bytes(k.as_bytes()).expect(\"Invalid header name from host\");\n            let value = http::HeaderValue::from_bytes(v.as_ref()).expect(\"Invalid header value from host\");\n            (name, value)\n        })\n        .collect();\n\n    http::Request::from_parts(parts, body)\n}\n\n#[cfg(feature = \"unstable\")]\npub(crate) fn response_into_wire(response: http::Response<Body>) -> (st_http::Response, Bytes) {\n    let (parts, body) = response.into_parts();\n    let st_response = st_http::Response {\n        headers: parts\n            .headers\n            .into_iter()\n            .map(|(k, v)| (k.map(|k| k.as_str().into()), v.as_bytes().into()))\n            .collect(),\n        version: match parts.version {","sourceCodeStart":635,"sourceCodeEnd":671,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/524b4487d949b61a07d4f39c862d1290259dfd20/crates/bindings/src/http.rs#L635-L671","documentation":"On the host-to-guest HTTP bridge, header names arriving from the host are converted with http::HeaderName::from_bytes, which accepts only HTTP token characters. A name containing spaces, control bytes, or non-ASCII characters, or an empty name, is rejected - and this .expect converts that rejection into a panic that aborts the call.","triggerScenarios":"The host supplies a request whose header name is malformed: a space instead of a hyphen (\"Content Type\"), non-ASCII/UTF-8 names, control characters, or the empty string - typically unvalidated client or gateway input forwarded into the module's HTTP API.","commonSituations":"Proxies/gateways injecting malformed headers; header names built from unvalidated user input; fuzzed HTTP requests reaching the bridge.","solutions":["Sanitize header names to ASCII token characters before they reach the bridge; drop invalid ones with a logged warning.","Validate and reject malformed headers (HTTP 400) at your API boundary instead of letting them reach the module.","Upgrade bindings - newer versions may return an error instead of panicking."],"exampleFix":"// before: panics on malformed input\nlet name = http::HeaderName::from_bytes(k.as_bytes()).expect(\"Invalid header name from host\");\n\n// after: validate and skip\nlet Ok(name) = http::HeaderName::from_bytes(k.as_bytes()) else {\n    log::warn!(\"dropping invalid header name: {k:?}\");\n    continue;\n};","handlingStrategy":"type-guard","validationCode":"// Reject malformed header names at your boundary before they are forwarded:\nfn header_name_ok(name: &str) -> bool {\n    !name.is_empty()\n        && name.bytes().all(|b| matches!(b, 33..=126) && !b\"()<>@,;:\\\\\"/[]?={}\".contains(&b))\n        && b' ' != 32 // spaces excluded by the 33..=126 range above\n}","typeGuard":"fn valid_header_name(name: &str) -> bool {\n    !name.is_empty() && name.bytes().all(|b| b > 32 && b < 127 && !b\"()<>@,;:\\\\\"/[]?={}\".contains(&b))\n}","tryCatchPattern":"// It panics, not returns Err: catch it at the embedding boundary.\nlet outcome = std::panic::catch_unwind(|| handle_request(request));\nif outcome.is_err() {\n    // Trap already unwound this call: log the offending headers, respond 400/500,\n    // and keep the process alive.\n}","preventionTips":["Never build header names from unvalidated user or gateway input.","Restrict names to ASCII token characters; reject non-ASCII and control bytes early.","Fuzz your HTTP boundary so malformed names are rejected before reaching the bridge."],"tags":["http","headers","panic","wasm","rust"],"backgroundTag":"invalid-http-header","analyzedSha":"524b4487d949b61a07d4f39c862d1290259dfd20","analyzedAt":"2026-08-16T23:58:54.611Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}