Expand description
⚠️ This crate is in development and may not work as expected yet.
§Overview
Streamlined webhook validation in Rocket applications.
- Automatically validate and deserialize webhook JSON payloads using the WebhookPayload data guard. You can also get the raw body using WebhookPayloadRaw.
- Common webhooks included (GitHub, Slack, Stripe, Standard)
- Easily validate custom webhooks with one of the generic builders
§Usage
use rocket::{routes, post, serde::{Serialize, Deserialize}};
use rocket_webhook::{
RocketWebhook, WebhookPayload,
webhooks::built_in::{GitHubWebhook, SlackWebhook},
};
#[rocket::launch]
fn rocket() -> _ {
// Build the webhook(s)
let github_webhook = RocketWebhook::builder()
.webhook(GitHubWebhook::with_secret(b"my-github-secret"))
.build();
let slack_webhook = RocketWebhook::builder()
.webhook(SlackWebhook::with_secret(b"my-slack-secret"))
.build();
// Store the webhook(s) in Rocket state
let rocket = rocket::build()
.manage(github_webhook)
.manage(slack_webhook)
.mount("/", routes![github_route]);
rocket
}
/// JSON payload to deserialize
#[derive(Debug, Serialize, Deserialize)]
struct GithubPayload {
action: String,
}
// use the `WebhookPayload` data guard in a route handler
#[post("/api/webhooks/github", data = "<payload>")]
async fn github_route(
payload: WebhookPayload<'_, GithubPayload, GitHubWebhook>,
) -> &'static str {
payload.data; // access the validated webhook payload
payload.headers; // access the webhook headers
"OK"
}
§Custom webhooks
If you’re using a webhook provider that is not built-in, there are two ways to integrate them:
§Use generic builder
This is the preferred (and simpler) approach - use one of the generic webhook builders to build a webhook
for your provider/service. For example, here is a custom webhook that expects a hex-encoded HMAC SHA256 signature
in the Foo-Signature-256
header.
use rocket_webhook::{WebhookError, webhooks::generic::Hmac256Webhook};
let my_webhook = Hmac256Webhook::builder()
.secret("my-secret")
.expected_signatures(|req| {
req.headers()
.get_one("Foo-Signature-256")
.and_then(|header| hex::decode(header).ok())
.map(|header| vec![header])
})
.build();
§Implement webhook traits
If a generic builder is not available, you can directly implement one of the signature traits along with the Webhook trait. See the implementations in webhooks::built_in for examples.
§Handling errors
By default, the webhook data guards will return Bad Request (400) for invalid requests (e.g. missing headers) and Unauthorized (401) for signature validation failures. Rocket’s error responses can be overridden using catchers scoped to a specific path.
If you need more control over how to process and respond to webhook errors, you can wrap the data guards with a Result, using the WebhookError as the Error type. You can then match on the result and handle the response as desired.
use rocket::{routes, post, serde::{Serialize, Deserialize}};
use rocket::http::Status;
use rocket_webhook::{
WebhookError, WebhookPayload,
webhooks::built_in::{GitHubWebhook},
};
#[post("/api/webhooks/github", data = "<payload_result>")]
async fn github_route(
payload_result: Result<WebhookPayload<'_, GithubPayload, GitHubWebhook>, WebhookError>,
) -> (Status, &'static str) {
match payload_result {
Ok(payload) => (Status::Ok, "Yay!"),
Err(err) => match err {
WebhookError::Signature(_) => (Status::Unauthorized, "Yikes!"),
_ => (Status::UnprocessableEntity, "Oof!")
}
}
}
/// Payload to deserialize
#[derive(Debug, Serialize, Deserialize)]
struct GithubPayload {
action: String,
}
§Multiple with same type
If you want to receive webhooks using multiple accounts/keys from the same built-in or generic webhook, you’ll need to pass in a marker struct when building the webhooks and using the data guards. This is needed to distinguish between the two webhooks in Rocket’s internal state.
use rocket::{get, routes};
use rocket_webhook::{
RocketWebhook, WebhookPayloadRaw, webhooks::built_in::SlackWebhook,
};
// Create a marker struct for each account/key
struct SlackAccount1;
struct SlackAccount2;
fn two_slack_accounts() {
// Use the `builder_with_marker` function
let slack_1 = RocketWebhook::builder_with_marker()
.webhook(SlackWebhook::with_secret("slack-1-secret"))
.marker(SlackAccount1) // pass in the marker here
.build();
let slack_2 = RocketWebhook::builder_with_marker()
.webhook(SlackWebhook::with_secret("slack-2-secret"))
.marker(SlackAccount2) // pass in the marker here
.build();
let rocket = rocket::build()
.manage(slack_1)
.manage(slack_2)
.mount("/", routes![slack1_route, slack2_route]);
}
// Use the marker struct as the last type parameter in the data guard:
#[get("/slack-1", data = "<payload>")]
async fn slack1_route(payload: WebhookPayloadRaw<'_, SlackWebhook, SlackAccount1>) -> Vec<u8> {
payload.data
}
#[get("/slack-2", data = "<payload>")]
async fn slack2_route(payload: WebhookPayloadRaw<'_, SlackWebhook, SlackAccount2>) -> Vec<u8> {
payload.data
}
Modules§
- webhooks
- Webhook traits and implementations
Structs§
- Rocket
Webhook - Webhook configuration stored in Rocket state.
- Webhook
Payload - Data guard to validate and deserialize the JSON body of webhook type
W
into theT
type. TheW
webhook configuration must be in Rocket state using RocketWebhook. - Webhook
Payload Raw - Data guard to validate a webhook and get the raw body.
The
W
webhook configuration must be in Rocket state using RocketWebhook.
Enums§
- Webhook
Error - Possible errors when receiving a webhook