risingwavelabs/risingwave · error

`SECURE_COMPARE()` failed

Error message

`SECURE_COMPARE()` failed

What it means

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.

Solutions

  1. Always send the signature header with the request.
  2. Verify the secret expression/column in the table definition is not NULL.
  3. Check the payload for fields the signature depends on being present.
  4. Treat as 400: fix the request rather than retrying.

Example fix

// before
curl -X POST http://host/v1/tables/123/webhook -d '{...}' # no signature header
// after
curl -X POST http://host/v1/tables/123/webhook -H "x-webhook-signature: <hmac>" -d '{...}'
Defensive patterns

Strategy: try-catch

Validate before calling

if (!headers.get('signature')) throw new Error('signature header is required for webhook requests');

Try / catch

const res = await post(url, body, headers);
if (res.status === 400 && (await res.text()).includes('SECURE_COMPARE')) {
  // signature input was NULL: attach the signature header and retry once
}

Prevention

When it happens

Trigger: 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.

Common situations: Client omits the signature header entirely, secret material resolving to NULL, misconfigured webhook table definition where the signature column/expression evaluates to NULL.

Related errors


AI-assisted analysis of risingwavelabs/risingwave@6469eb736d (2026-09-11). Data as JSON: /api/errors/915df0daa9cea105. Report an issue: GitHub.

Appendix: source

Thrown at src/frontend/src/webhook/utils.rs:156

    payload: &[u8],
    signature_expr: ExprNode,
) -> Result<bool> {
    let row = OwnedRow::new(vec![
        Some(headers_jsonb.into()),
        Some(secret.into()),
        Some(payload.into()),
    ]);

    let signature_expr_impl = ExprImpl::from_expr_proto(&signature_expr)
        .map_err(|e| err(e, StatusCode::INTERNAL_SERVER_ERROR))?;

    let result = signature_expr_impl
        .eval_row(&row)
        .await
        .map_err(|e| err(e, StatusCode::INTERNAL_SERVER_ERROR))?
        .ok_or_else(|| {
            err(
                anyhow!("`SECURE_COMPARE()` failed"),
                StatusCode::BAD_REQUEST,
            )
        })?;
    Ok(*result.as_bool())
}

#[cfg(test)]
mod tests {
    use axum::body::to_bytes;
    use axum::http::header::HeaderName;

    use super::*;

    #[tokio::test]
    async fn test_webhook_error_response() {
        let response = err(
            anyhow!("failed to decode webhook payload"),
            StatusCode::UNPROCESSABLE_ENTITY,

View on GitHub (pinned to 6469eb736d)