fraiseql_error/webhook.rs
1/// Errors that occur while receiving and validating inbound webhook requests.
2#[derive(Debug, thiserror::Error)]
3#[non_exhaustive]
4pub enum WebhookError {
5 /// The HMAC signature on the webhook payload does not match the expected
6 /// value for the shared secret.
7 ///
8 /// This can indicate an invalid secret, payload tampering, or a replay
9 /// from a different provider.
10 #[error("Invalid signature")]
11 InvalidSignature,
12
13 /// The expected signature header was absent from the incoming request.
14 #[error("Missing signature header: {header}")]
15 MissingSignature {
16 /// Name of the HTTP header that was expected but not present.
17 header: String,
18 },
19
20 /// The webhook timestamp is older than the configured replay-window,
21 /// indicating a replay attack or severe clock skew.
22 #[error("Timestamp too old: {age_seconds}s (max: {max_seconds}s)")]
23 TimestampExpired {
24 /// Age of the webhook event in seconds.
25 age_seconds: u64,
26 /// Maximum allowed age in seconds.
27 max_seconds: u64,
28 },
29
30 /// The webhook timestamp is further in the future than clock skew allows,
31 /// suggesting a pre-generated or tampered request.
32 #[error("Timestamp in future: {future_seconds}s")]
33 TimestampFuture {
34 /// Number of seconds the timestamp is ahead of the server clock.
35 future_seconds: u64,
36 },
37
38 /// An event with this identifier has already been successfully processed.
39 ///
40 /// The event should be acknowledged (2xx) and then discarded.
41 #[error("Duplicate event: {event_id}")]
42 DuplicateEvent {
43 /// Identifier of the duplicate event.
44 event_id: String,
45 },
46
47 /// The event's `type` field does not correspond to any registered handler.
48 #[error("Unknown event type: {event_type}")]
49 UnknownEvent {
50 /// The unrecognised event type string.
51 event_type: String,
52 },
53
54 /// A webhook was received from a provider that has not been configured
55 /// in `fraiseql.toml`.
56 #[error("Provider not configured: {provider}")]
57 ProviderNotConfigured {
58 /// Name of the unconfigured provider.
59 provider: String,
60 },
61
62 /// The webhook request body could not be parsed (invalid JSON, unexpected
63 /// schema, etc.).
64 ///
65 /// The raw error message is kept server-side and a generic response is
66 /// returned to the caller.
67 #[error("Payload parse error: {message}")]
68 PayloadError {
69 /// Description of the parse failure (server-side only).
70 message: String,
71 },
72
73 /// The idempotency check (deduplication store lookup or write) failed.
74 #[error("Idempotency check failed: {message}")]
75 IdempotencyError {
76 /// Description of the idempotency failure.
77 message: String,
78 },
79}
80
81impl WebhookError {
82 /// Returns a short, stable error code string suitable for API responses and
83 /// structured logging.
84 pub const fn error_code(&self) -> &'static str {
85 match self {
86 Self::InvalidSignature => "webhook_invalid_signature",
87 Self::MissingSignature { .. } => "webhook_missing_signature",
88 Self::TimestampExpired { .. } => "webhook_timestamp_expired",
89 Self::TimestampFuture { .. } => "webhook_timestamp_future",
90 Self::DuplicateEvent { .. } => "webhook_duplicate_event",
91 Self::UnknownEvent { .. } => "webhook_unknown_event",
92 Self::ProviderNotConfigured { .. } => "webhook_provider_not_configured",
93 Self::PayloadError { .. } => "webhook_payload_error",
94 Self::IdempotencyError { .. } => "webhook_idempotency_error",
95 }
96 }
97}