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