Expand description
Webhooks: signatures for calls in both directions, and processing each event once.
A payment provider, a GPU service or a mail service calls the app back
with a signed POST. The handler checks the signature over the raw body,
then runs the effect inside once, which records the event in the
webhook_events table (ocre g webhook creates it) and skips a
delivery it has already processed: providers retry until they get a 2xx,
so the same event often arrives twice.
use axum::{body::Bytes, extract::State, http::HeaderMap};
use ocre::{Ctx, Result, webhooks};
async fn receive(State(ctx): State<Ctx>, headers: HeaderMap, body: Bytes) -> Result<&'static str> {
let secret = ctx.secret("PAYMENTS_WEBHOOK_SECRET").await?;
let signature = headers.get("x-signature").and_then(|v| v.to_str().ok()).unwrap_or_default();
webhooks::verify(secret.as_bytes(), &body, signature)?;
let event: serde_json::Value =
serde_json::from_slice(&body).map_err(|_| ocre::Error::bad_request("invalid JSON"))?;
let id = event["id"].as_str().unwrap_or_default().to_owned();
let db = ctx.db()?;
webhooks::once(&db, "payments", &id, &body, || async {
// ... mark the order paid: runs once per event id
Ok(())
})
.await?;
Ok("ok")
}sign is the other direction: the app signs what it sends (a job
submitted to an external service), and the service checks it the same way.
Structs§
- Answer
- The answer of
post_signed: status and body.
Enums§
Constants§
- STALE_
AFTER - A delivery left
processingthis long (seconds) is taken over by the next one: the invocation that claimed it died before finishing. - TABLE_
SQL - The
webhook_eventstableoncerecords deliveries in, created by the migration ofocre g webhook.
Functions§
- once
- Runs
effectfor the eventevent_idofsourceunless it already ran. - post_
signed - POSTs
bodyas JSON tourl, signed: the HMAC-SHA256 of the body withsecretinX-Signature(lowercase hex, seesign), andAuthorization: Bearer <bearer>when given (API keys of services such as RunPod). One subrequest. Any status is an answer: checkstatus(a service’s 4xx is not an error of the call). - sign
- HMAC-SHA256 of
messagewithsecret, as lowercase hex: the signature to send in a header (X-Signature) of an outgoing call. - sign_
standard - The
webhook-signatureheader value (v1,<base64>) of a Standard Webhooks delivery ofbodywith thisidandtimestamp(Unix seconds): whatverify_standardchecks. For an app that sends Standard Webhooks, and for tests of one that receives them. - verify
- Checks that
signatureis the HMAC-SHA256 ofmessagewithsecret, in constant time. The signature may be hex (any case, optionally prefixedsha256=as GitHub sends it) or base64 (standard or URL-safe, padded or not), the common encodings of webhook providers. Sign the raw request body: parsed and re-serialized JSON may differ by a space. - verify_
standard - Checks a Standard Webhooks delivery
(Svix, Resend, and other providers):
webhook-signatureholds one or more space-separatedv1,<base64>signatures of{id}.{timestamp}.{body}keyed with the base64 part of thewhsec_...secret, and thewebhook-timestampmust be withintoleranceseconds ofnow(replays of an old delivery are refused). Returns thewebhook-id, the event id to giveonce.