{"record":{"id":"915df0daa9cea105","repo":"risingwavelabs/risingwave","slug":"secure-compare-failed","errorCode":null,"errorMessage":"`SECURE_COMPARE()` failed","messagePattern":"`SECURE_COMPARE\\(\\)` failed","errorType":"http","errorClass":null,"httpStatus":400,"severity":"error","filePath":"src/frontend/src/webhook/utils.rs","lineNumber":156,"sourceCode":"    payload: &[u8],\n    signature_expr: ExprNode,\n) -> Result<bool> {\n    let row = OwnedRow::new(vec![\n        Some(headers_jsonb.into()),\n        Some(secret.into()),\n        Some(payload.into()),\n    ]);\n\n    let signature_expr_impl = ExprImpl::from_expr_proto(&signature_expr)\n        .map_err(|e| err(e, StatusCode::INTERNAL_SERVER_ERROR))?;\n\n    let result = signature_expr_impl\n        .eval_row(&row)\n        .await\n        .map_err(|e| err(e, StatusCode::INTERNAL_SERVER_ERROR))?\n        .ok_or_else(|| {\n            err(\n                anyhow!(\"`SECURE_COMPARE()` failed\"),\n                StatusCode::BAD_REQUEST,\n            )\n        })?;\n    Ok(*result.as_bool())\n}\n\n#[cfg(test)]\nmod tests {\n    use axum::body::to_bytes;\n    use axum::http::header::HeaderName;\n\n    use super::*;\n\n    #[tokio::test]\n    async fn test_webhook_error_response() {\n        let response = err(\n            anyhow!(\"failed to decode webhook payload\"),\n            StatusCode::UNPROCESSABLE_ENTITY,","sourceCodeStart":138,"sourceCodeEnd":174,"githubUrl":"https://github.com/risingwavelabs/risingwave/blob/6469eb736d691e8e9b8a419a57edd6429ca77417/src/frontend/src/webhook/utils.rs#L138-L174","documentation":"SECURE_COMPARE is the internal expression used to compare the expected and provided webhook signatures. If its row evaluation returns NULL (an Option::None result), RisingWave maps that to this 400 BAD_REQUEST error. This typically means one of the comparison inputs was NULL, e.g. the client sent no signature.","triggerScenarios":"verify_signature evaluates the SECURE_COMPARE expression but eval_row returns Ok(None) — usually when the signature header is absent or one operand of the comparison is NULL.","commonSituations":"Client omits the signature header entirely, secret material resolving to NULL, misconfigured webhook table definition where the signature column/expression evaluates to NULL.","solutions":["Always send the signature header with the request.","Verify the secret expression/column in the table definition is not NULL.","Check the payload for fields the signature depends on being present.","Treat as 400: fix the request rather than retrying."],"exampleFix":"// before\ncurl -X POST http://host/v1/tables/123/webhook -d '{...}' # no signature header\n// after\ncurl -X POST http://host/v1/tables/123/webhook -H \"x-webhook-signature: <hmac>\" -d '{...}'","handlingStrategy":"try-catch","validationCode":"if (!headers.get('signature')) throw new Error('signature header is required for webhook requests');","typeGuard":null,"tryCatchPattern":"const res = await post(url, body, headers);\nif (res.status === 400 && (await res.text()).includes('SECURE_COMPARE')) {\n  // signature input was NULL: attach the signature header and retry once\n}","preventionTips":["Never omit the signature header on webhook requests.","Ensure the secret column/expression in the table definition is never NULL.","Validate the payload against the schema before sending so signature inputs are present."],"tags":["security","webhook","signature","null"],"backgroundTag":"signature-verification-failed","analyzedSha":"6469eb736d691e8e9b8a419a57edd6429ca77417","analyzedAt":"2026-09-11T21:06:21.487Z","contentChangedAt":"2026-09-11T21:06:21.487Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}