Skip to main content

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}