{"record":{"id":"3be6398eae4c78ff","repo":"risingwavelabs/risingwave","slug":"http-sink-only-supports-append-only-mode","errorCode":null,"errorMessage":"HTTP sink only supports append-only mode","messagePattern":"HTTP sink only supports append-only mode","errorType":"validation","errorClass":"SinkError::Config","httpStatus":null,"severity":"error","filePath":"src/connector/src/sink/http.rs","lineNumber":80,"sourceCode":"    pub fn from_btreemap(\n        values: BTreeMap<String, String>,\n    ) -> Result<(Self, BTreeMap<String, String>)> {\n        // Extract header.* keys before serde parsing\n        let mut headers = BTreeMap::new();\n        let mut rest = BTreeMap::new();\n        for (k, v) in &values {\n            if let Some(header_name) = k.strip_prefix(\"header.\") {\n                headers.insert(header_name.to_owned(), v.clone());\n            } else {\n                rest.insert(k.clone(), v.clone());\n            }\n        }\n\n        let config = serde_json::from_value::<HttpConfig>(serde_json::to_value(rest).unwrap())\n            .map_err(|e| SinkError::Config(anyhow!(e)))?;\n\n        if config.r#type != SINK_TYPE_APPEND_ONLY {\n            return Err(SinkError::Config(anyhow!(\n                \"HTTP sink only supports append-only mode\"\n            )));\n        }\n\n        Ok((config, headers))\n    }\n}\n\n#[derive(Clone, Debug)]\nenum HttpUrl {\n    Static(reqwest::Url),\n    Dynamic { url_index: usize },\n}\n\n/// Validates the HTTP sink parameters and returns the sink so callers can use it directly without\n/// re-parsing.\nfn validate_http_sink(\n    is_append_only: bool,","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/connector/src/sink/http.rs#L62-L98","documentation":"Configuration rejection raised in HttpSink::from_btreemap: the HTTP sink can only write rows, never emit deletes or updates, so a sink whose declaration is not append-only (e.g. an upsert sink with a primary key) cannot be instantiated. It fires when the caller passes a non-append-only sink type while creating or altering an HTTP sink.","triggerScenarios":"`from_btreemap` parses HttpConfig whose `r#type` field does not equal SINK_TYPE_APPEND_ONLY (e.g. user set type='debezium' or an upsert variant).","commonSituations":"User configures an HTTP sink over a source with updates/deletes and sets type to something other than 'append-only'; copy-pasting sink configs from Kafka/Debezium examples.","solutions":["Set type='append-only' in the sink WITH options","Ensure the backing stream/MV is append-only (no UPDATE/DELETE)","Use a sink connector that supports upsert/debezium modes (e.g. JDBC/Kafka) if you need mutations"],"exampleFix":"// before\nWITH (connector='http', url='...', type='debezium')\n// after\nWITH (connector='http', url='...', type='append-only')","handlingStrategy":"validation","validationCode":"// reject non-append-only types before sink creation\nif let Some(t) = options.get(\"type\") {\n    if t != \"append-only\" { return Err(format!(\"HTTP sink requires type='append-only', got '{t}'\")); }\n}","typeGuard":null,"tryCatchPattern":"match HttpSink::from_btreemap(&props, ...) {\n    Err(SinkError::Config(e)) if e.to_string().contains(\"append-only\") => {\n        log::error!(\"HTTP sink configured over mutating stream: {e}\");\n    }\n    r => r?,\n}","preventionTips":["Always set type='append-only' explicitly for HTTP sinks","Only sink from append-only MVs","Use CDC-capable connectors when updates/deletes matter"],"tags":["http","sink","config","append-only"],"backgroundTag":"unsupported-config-value","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-14T16:17:12.679Z"}