You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: support standard webhook delivery mode (#519)
* feat: destwebhookstandard
* chore: destination webhook mode
* fix: metadata.json
* fix: destination type
* test: verify delivery using official standard-webhooks sdk
* feat: configurable webhook header prefix
* docs: generate config
* fix: prevent response body from being consumed twice in webhook error handling
When webhook requests fail (status >= 400), the response body was being
read twice - once with io.ReadAll() and again in parseResponse(). Since
HTTP response bodies are streams, the second read would get an empty
result, causing delivery.Response to have an empty body instead of the
actual error message.
This fix removes the duplicate read and lets parseResponse() handle all
body reading. The parsed body is then extracted from delivery.Response
for error metadata.
Affects:
- internal/destregistry/providers/destwebhook/destwebhook.go
- internal/destregistry/providers/destwebhookstandard/destwebhookstandard.go
Impact: Failed webhook deliveries will now correctly capture and display
the actual error response body from endpoints, improving debuggability.
Or a more concise version:
fix: prevent double-read of webhook response body on error
Response body was consumed twice in error path (status >= 400):
first by io.ReadAll(), then by parseResponse(). This caused empty
bodies in delivery responses, hiding actual error messages.
Fixed by removing the first read and extracting the body from
delivery.Response after parseResponse() completes.
Fixes both destwebhook and destwebhookstandard providers.
* chore: update metadata.json to be consistent with webhook destination
Copy file name to clipboardExpand all lines: docs/pages/references/configuration.mdx
+22-18Lines changed: 22 additions & 18 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -47,16 +47,17 @@ Global configurations are provided through env variables or a YAML file. ConfigM
47
47
|`DESTINATIONS_AWS_KINESIS_METADATA_IN_PAYLOAD`| If true, includes Outpost metadata (event ID, topic, etc.) within the Kinesis record payload. |`true`| No |
48
48
|`DESTINATIONS_INCLUDE_MILLISECOND_TIMESTAMP`| If true, includes a 'timestamp-ms' field with millisecond precision in destination metadata. Useful for load testing and debugging. |`false`| No |
49
49
|`DESTINATIONS_METADATA_PATH`| Path to the directory containing custom destination type definitions. This can be overridden by the root-level 'destination_metadata_path' if also set. |`config/outpost/destinations`| No |
50
-
|`DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_EVENT_ID_HEADER`| If true, disables adding the default 'X-Outpost-Event-Id' header to webhook requests. |`false`| No |
51
-
|`DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_SIGNATURE_HEADER`| If true, disables adding the default 'X-Outpost-Signature' header to webhook requests. |`false`| No |
52
-
|`DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TIMESTAMP_HEADER`| If true, disables adding the default 'X-Outpost-Timestamp' header to webhook requests. |`false`| No |
53
-
|`DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TOPIC_HEADER`| If true, disables adding the default 'X-Outpost-Topic' header to webhook requests. |`false`| No |
54
-
|`DESTINATIONS_WEBHOOK_HEADER_PREFIX`| Prefix for custom headers added to webhook requests (e.g., 'X-MyOrg-'). |`x-outpost-`| No |
50
+
|`DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_EVENT_ID_HEADER`| If true, disables adding the default 'X-Outpost-Event-Id' header to webhook requests. Only applies to 'default' mode. |`false`| No |
51
+
|`DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_SIGNATURE_HEADER`| If true, disables adding the default 'X-Outpost-Signature' header to webhook requests. Only applies to 'default' mode. |`false`| No |
52
+
|`DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TIMESTAMP_HEADER`| If true, disables adding the default 'X-Outpost-Timestamp' header to webhook requests. Only applies to 'default' mode. |`false`| No |
53
+
|`DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TOPIC_HEADER`| If true, disables adding the default 'X-Outpost-Topic' header to webhook requests. Only applies to 'default' mode. |`false`| No |
54
+
|`DESTINATIONS_WEBHOOK_HEADER_PREFIX`| Prefix for metadata headers added to webhook requests. Defaults to 'x-outpost-' in 'default' mode and 'webhook-' in 'standard' mode. |`x-outpost-`| No |
55
+
|`DESTINATIONS_WEBHOOK_MODE`| Webhook mode: 'default' for customizable webhooks or 'standard' for Standard Webhooks specification compliance. Defaults to 'default'. |`nil`| No |
55
56
|`DESTINATIONS_WEBHOOK_PROXY_URL`| Proxy URL for routing webhook requests through a proxy server. Supports HTTP and HTTPS proxies. When configured, all outgoing webhook traffic will be routed through the specified proxy. |`nil`| No |
56
-
|`DESTINATIONS_WEBHOOK_SIGNATURE_ALGORITHM`| Algorithm used for signing webhook requests (e.g., 'hmac-sha256'). |`hmac-sha256`| No |
57
-
|`DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE`| Go template for constructing the content to be signed for webhook requests. |`{{.Timestamp.Unix}}.{{.Body}}`| No |
58
-
|`DESTINATIONS_WEBHOOK_SIGNATURE_ENCODING`| Encoding for the signature (e.g., 'hex', 'base64'). |`hex`| No |
59
-
|`DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE`| Go template for the value of the signature header. |`t={{.Timestamp.Unix}},v0={{.Signatures \| join ","}}`| No |
57
+
|`DESTINATIONS_WEBHOOK_SIGNATURE_ALGORITHM`| Algorithm used for signing webhook requests (e.g., 'hmac-sha256'). Only applies to 'default' mode. |`hmac-sha256`| No |
58
+
|`DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE`| Go template for constructing the content to be signed for webhook requests. Only applies to 'default' mode. |`{{.Timestamp.Unix}}.{{.Body}}`| No |
59
+
|`DESTINATIONS_WEBHOOK_SIGNATURE_ENCODING`| Encoding for the signature (e.g., 'hex', 'base64'). Only applies to 'default' mode. |`hex`| No |
60
+
|`DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE`| Go template for the value of the signature header. Only applies to 'default' mode. |`t={{.Timestamp.Unix}},v0={{.Signatures \| join ","}}`| No |
60
61
|`DESTINATION_METADATA_PATH`| Path to the directory containing custom destination type definitions. Overrides 'destinations.metadata_path' if set. |`nil`| No |
61
62
|`DISABLE_TELEMETRY`| Global flag to disable all telemetry (anonymous usage statistics to Hookdeck and error reporting to Sentry). If true, overrides 'telemetry.disabled'. |`false`| No |
62
63
|`GCP_PUBSUB_DELIVERY_SUBSCRIPTION`| Name of the GCP Pub/Sub subscription for delivery events. |`outpost-delivery-sub`| No |
@@ -186,34 +187,37 @@ destinations:
186
187
187
188
# Configuration specific to webhook destinations.
188
189
webhook:
189
-
# If true, disables adding the default 'X-Outpost-Event-Id' header to webhook requests.
190
+
# If true, disables adding the default 'X-Outpost-Event-Id' header to webhook requests. Only applies to 'default' mode.
190
191
disable_default_event_id_header: false
191
192
192
-
# If true, disables adding the default 'X-Outpost-Signature' header to webhook requests.
193
+
# If true, disables adding the default 'X-Outpost-Signature' header to webhook requests. Only applies to 'default' mode.
193
194
disable_default_signature_header: false
194
195
195
-
# If true, disables adding the default 'X-Outpost-Timestamp' header to webhook requests.
196
+
# If true, disables adding the default 'X-Outpost-Timestamp' header to webhook requests. Only applies to 'default' mode.
196
197
disable_default_timestamp_header: false
197
198
198
-
# If true, disables adding the default 'X-Outpost-Topic' header to webhook requests.
199
+
# If true, disables adding the default 'X-Outpost-Topic' header to webhook requests. Only applies to 'default' mode.
199
200
disable_default_topic_header: false
200
201
201
-
# Prefix for custom headers added to webhook requests (e.g., 'X-MyOrg-').
202
+
# Prefix for metadata headers added to webhook requests. Defaults to 'x-outpost-' in 'default' mode and 'webhook-' in 'standard' mode.
202
203
header_prefix: "x-outpost-"
203
204
205
+
# Webhook mode: 'default' for customizable webhooks or 'standard' for Standard Webhooks specification compliance. Defaults to 'default'.
206
+
mode: ""
207
+
204
208
# Proxy URL for routing webhook requests through a proxy server. Supports HTTP and HTTPS proxies. When configured, all outgoing webhook traffic will be routed through the specified proxy.
205
209
proxy_url: ""
206
210
207
-
# Algorithm used for signing webhook requests (e.g., 'hmac-sha256').
211
+
# Algorithm used for signing webhook requests (e.g., 'hmac-sha256'). Only applies to 'default' mode.
208
212
signature_algorithm: "hmac-sha256"
209
213
210
-
# Go template for constructing the content to be signed for webhook requests.
214
+
# Go template for constructing the content to be signed for webhook requests. Only applies to 'default' mode.
Copy file name to clipboardExpand all lines: internal/config/destinations.go
+15-9Lines changed: 15 additions & 9 deletions
Original file line number
Diff line number
Diff line change
@@ -38,21 +38,27 @@ type DestinationWebhookConfig struct {
38
38
// ProxyURL may contain authentication credentials (e.g., http://user:pass@proxy:8080)
39
39
// and should be treated as sensitive.
40
40
// TODO: Implement sensitive value handling - https://github.com/hookdeck/outpost/issues/480
41
+
Modestring`yaml:"mode" env:"DESTINATIONS_WEBHOOK_MODE" desc:"Webhook mode: 'default' for customizable webhooks or 'standard' for Standard Webhooks specification compliance. Defaults to 'default'." required:"N"`
41
42
ProxyURLstring`yaml:"proxy_url" env:"DESTINATIONS_WEBHOOK_PROXY_URL" desc:"Proxy URL for routing webhook requests through a proxy server. Supports HTTP and HTTPS proxies. When configured, all outgoing webhook traffic will be routed through the specified proxy." required:"N"`
42
-
HeaderPrefixstring`yaml:"header_prefix" env:"DESTINATIONS_WEBHOOK_HEADER_PREFIX" desc:"Prefix for custom headers added to webhook requests (e.g., 'X-MyOrg-')." required:"N"`
43
-
DisableDefaultEventIDHeaderbool`yaml:"disable_default_event_id_header" env:"DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_EVENT_ID_HEADER" desc:"If true, disables adding the default 'X-Outpost-Event-Id' header to webhook requests." required:"N"`
44
-
DisableDefaultSignatureHeaderbool`yaml:"disable_default_signature_header" env:"DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_SIGNATURE_HEADER" desc:"If true, disables adding the default 'X-Outpost-Signature' header to webhook requests." required:"N"`
45
-
DisableDefaultTimestampHeaderbool`yaml:"disable_default_timestamp_header" env:"DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TIMESTAMP_HEADER" desc:"If true, disables adding the default 'X-Outpost-Timestamp' header to webhook requests." required:"N"`
46
-
DisableDefaultTopicHeaderbool`yaml:"disable_default_topic_header" env:"DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TOPIC_HEADER" desc:"If true, disables adding the default 'X-Outpost-Topic' header to webhook requests." required:"N"`
47
-
SignatureContentTemplatestring`yaml:"signature_content_template" env:"DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE" desc:"Go template for constructing the content to be signed for webhook requests." required:"N"`
48
-
SignatureHeaderTemplatestring`yaml:"signature_header_template" env:"DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE" desc:"Go template for the value of the signature header." required:"N"`
49
-
SignatureEncodingstring`yaml:"signature_encoding" env:"DESTINATIONS_WEBHOOK_SIGNATURE_ENCODING" desc:"Encoding for the signature (e.g., 'hex', 'base64')." required:"N"`
50
-
SignatureAlgorithmstring`yaml:"signature_algorithm" env:"DESTINATIONS_WEBHOOK_SIGNATURE_ALGORITHM" desc:"Algorithm used for signing webhook requests (e.g., 'hmac-sha256')." required:"N"`
43
+
HeaderPrefixstring`yaml:"header_prefix" env:"DESTINATIONS_WEBHOOK_HEADER_PREFIX" desc:"Prefix for metadata headers added to webhook requests. Defaults to 'x-outpost-' in 'default' mode and 'webhook-' in 'standard' mode." required:"N"`
44
+
DisableDefaultEventIDHeaderbool`yaml:"disable_default_event_id_header" env:"DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_EVENT_ID_HEADER" desc:"If true, disables adding the default 'X-Outpost-Event-Id' header to webhook requests. Only applies to 'default' mode." required:"N"`
45
+
DisableDefaultSignatureHeaderbool`yaml:"disable_default_signature_header" env:"DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_SIGNATURE_HEADER" desc:"If true, disables adding the default 'X-Outpost-Signature' header to webhook requests. Only applies to 'default' mode." required:"N"`
46
+
DisableDefaultTimestampHeaderbool`yaml:"disable_default_timestamp_header" env:"DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TIMESTAMP_HEADER" desc:"If true, disables adding the default 'X-Outpost-Timestamp' header to webhook requests. Only applies to 'default' mode." required:"N"`
47
+
DisableDefaultTopicHeaderbool`yaml:"disable_default_topic_header" env:"DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_TOPIC_HEADER" desc:"If true, disables adding the default 'X-Outpost-Topic' header to webhook requests. Only applies to 'default' mode." required:"N"`
48
+
SignatureContentTemplatestring`yaml:"signature_content_template" env:"DESTINATIONS_WEBHOOK_SIGNATURE_CONTENT_TEMPLATE" desc:"Go template for constructing the content to be signed for webhook requests. Only applies to 'default' mode." required:"N"`
49
+
SignatureHeaderTemplatestring`yaml:"signature_header_template" env:"DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_TEMPLATE" desc:"Go template for the value of the signature header. Only applies to 'default' mode." required:"N"`
50
+
SignatureEncodingstring`yaml:"signature_encoding" env:"DESTINATIONS_WEBHOOK_SIGNATURE_ENCODING" desc:"Encoding for the signature (e.g., 'hex', 'base64'). Only applies to 'default' mode." required:"N"`
51
+
SignatureAlgorithmstring`yaml:"signature_algorithm" env:"DESTINATIONS_WEBHOOK_SIGNATURE_ALGORITHM" desc:"Algorithm used for signing webhook requests (e.g., 'hmac-sha256'). Only applies to 'default' mode." required:"N"`
51
52
}
52
53
53
54
// toConfig converts WebhookConfig to the provider config - private since it's only used internally
To receive events from the webhook destination, you need to set up a webhook endpoint.
4
+
5
+
A webhook endpoint is a URL that you provide to an HTTP server. When an event is sent to the webhook destination, an HTTP POST request is made to the webhook endpoint with a JSON payload. Information such as the event type will be sent in the HTTP headers.
6
+
7
+
## Verifying Webhook Signatures
8
+
9
+
Webhooks include a cryptographic signature for security. To verify:
10
+
11
+
1. Extract the `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers
12
+
2. Construct the signed content: `${webhook-id}.${webhook-timestamp}.${raw_body}`
13
+
3. Decode your `whsec_` secret (remove prefix and base64 decode)
14
+
4. Compute HMAC-SHA256 signature and compare
15
+
16
+
Verification libraries are available at: https://github.com/standard-webhooks
0 commit comments