{"record":{"id":"cc77a70425d05eb7","repo":"headroomlabs-ai/headroom","slug":"bedrock-eventstream-missing-event-type","errorCode":"bedrock_eventstream_missing_event_type","errorMessage":"Bedrock message missing required `:event-type` header","messagePattern":"Bedrock message missing required `:event-type` header","errorType":"exception","errorClass":"TranslateError::MissingEventType","httpStatus":null,"severity":"error","filePath":"crates/headroom-proxy/src/bedrock/eventstream_to_sse.rs","lineNumber":115,"sourceCode":"    /// `:event-type` values that AWS emits for protocol-internal\n    /// signalling (not yet observed for Bedrock Anthropic responses,\n    /// but we surface a structured outcome rather than guess).\n    Skip { event_type: String },\n}\n\n/// Errors during translation. Per project rules these are loud — the\n/// handler 5xx's the client when an unknown `:message-type` arrives,\n/// rather than silently swallowing.\n#[derive(Debug, thiserror::Error)]\npub enum TranslateError {\n    /// `:message-type == exception`. AWS reserved value indicating the\n    /// service raised a synchronous error mid-stream. Surfaced as a\n    /// structured error so the handler can map it to a 5xx + log.\n    #[error(\"Bedrock stream emitted exception: {payload_preview}\")]\n    UpstreamException { payload_preview: String },\n    /// The translator's input message is missing a required header\n    /// (`:event-type`). Wire-format violation — AWS would never emit.\n    #[error(\"Bedrock message missing required `:event-type` header\")]\n    MissingEventType,\n}\n\n/// Translate one EventStream message under the chosen output mode.\n///\n/// Side-effect-free: emits a `tracing::info!` per `chunk` translation\n/// and `tracing::warn!` for unknown event types. The hot path is\n/// allocation-bounded — one `BytesMut` of `payload.len() + 8`.\npub fn translate_message(\n    message: &EventStreamMessage,\n    mode: OutputMode,\n) -> Result<TranslateOutcome, TranslateError> {\n    // Always check for `:message-type == exception` first — that's a\n    // structural error regardless of mode. AWS reserves this value to\n    // indicate a service-side fault ON the stream (vs an HTTP-level\n    // error from the initial request).\n    if matches!(message.message_type(), Some(\"exception\")) {\n        let preview = String::from_utf8_lossy(&message.payload[..message.payload.len().min(160)])","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/crates/headroom-proxy/src/bedrock/eventstream_to_sse.rs#L97-L133","documentation":"The translator received an EventStream message that lacks the required :event-type header. Every AWS eventstream application message must carry :message-type and (for 'event' messages) :event-type; a message without it violates the wire format — AWS itself would never emit one. The handler 5xx's rather than guessing, per the loud-error policy.","triggerScenarios":"translate_message() called on a decoded EventStreamMessage whose headers map has no :event-type key; typically caused by a decoder bug that dropped headers, a hand-constructed message in tests missing the header, or a non-AWS eventstream source that follows the framing but not the Bedrock header contract.","commonSituations":"Unit tests building EventStreamMessage values by hand without the :event-type header; a fork/change to the decoder that loses headers on a re-parse path; feeding generic eventstream data (e.g. from another AWS service) into the Bedrock translator.","solutions":["If this fires in tests, add the \":event-type\" header (e.g. chunk, message_start) to the constructed message.","If it fires in production, log the full header set of the offending message — headers that are present but misnamed (typo'd or non-lowercase) are the usual cause.","Verify the decoded message came from the Bedrock response stream and not some other eventstream-carried protocol; the translator only understands Bedrock's event vocabulary.","Check for decoder regressions where headers_len from the prelude is ignored or truncated."],"exampleFix":"// before (test fixture)\nlet msg = EventStreamMessage { headers: vec![], payload: chunk_payload };\n\n// after\nlet msg = EventStreamMessage {\n    headers: vec![(\":event-type\".into(), \"chunk\".into())],\n    payload: chunk_payload,\n};","handlingStrategy":"validation","validationCode":"// Before translating, require the header\nuse std::collections::HashMap;\nfn has_event_type(headers: &HashMap<String, String>) -> bool {\n    headers.contains_key(\":event-type\")\n}","typeGuard":"fn is_translatable(msg: &EventStreamMessage) -> bool {\n    msg.headers().any(|(k, _)| k == \":event-type\")\n}","tryCatchPattern":"match translate_message(&msg, mode) {\n    Err(TranslateError::MissingEventType) => {\n        tracing::error!(?msg, \"wire-format violation from upstream; failing loudly\");\n        Err(UpstreamProtocolError) // handler 5xx's per project rule\n    }\n    r => r,\n}","preventionTips":["In tests, always construct messages with an :event-type header.","Assert on the full header set when this fires — misnamed headers are the usual cause.","Only feed Bedrock-originated streams to this translator."],"tags":["bedrock","eventstream","wire-format","sse","rust"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}