Skip to content

feat(webhook): configurable system header names, deprecate DISABLE_* flags#992

Open
ambroziepaval wants to merge 1 commit into
hookdeck:mainfrom
ambroziepaval:feat/webhook-signature-header-name
Open

feat(webhook): configurable system header names, deprecate DISABLE_* flags#992
ambroziepaval wants to merge 1 commit into
hookdeck:mainfrom
ambroziepaval:feat/webhook-signature-header-name

Conversation

@ambroziepaval

Copy link
Copy Markdown

Add the global DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_NAME config option. When set, its exact value becomes the signature header name, bypassing the "signature" construction; when unset, behavior is unchanged, so the change is fully backwards compatible.

Scoped to default mode only: standard-webhooks mode fixes the header as "webhook-signature" per spec, and renaming it would break spec-compliant verifiers. This mirrors every other signature-customization field and the existing SignatureHeaderTemplate plumbing. The name is validated at construction against the header-name regex and reserved-header set.

Issue #982.

@alexbouchardd

Copy link
Copy Markdown
Contributor

As mentioned in #982 (comment), I do not believe that's the right approach, since we now have 2 configs that implement essentially the same thing. The approach should also be header-agnostic and cover every header value.

We also need to account for backward compatibility of existing deployments, possibly by keeping the DISABLE_* config but marking it as deprecated and printing a warning.

@ambroziepaval

Copy link
Copy Markdown
Author

Thanks @alexbouchardd , this makes sense and I want to confirm I've got your point right. The signature header already has two configs that overlap: DISABLE_DEFAULT_SIGNATURE_HEADER (emit or not) and SIGNATURE_HEADER_TEMPLATE (the value). "Disable" and "empty value" are the same outcome, and my new SIGNATURE_HEADER_NAME piles a third knob onto the same header.

Concrete step I'd take now: keep the two configs that do distinct things: SIGNATURE_HEADER_NAME (the header name) and SIGNATURE_HEADER_TEMPLATE (the value), and treat an empty template (SIGNATURE_HEADER_TEMPLATE='') as "no signature emitted". So we go from three signature knobs down to two, with no overlap. DISABLE_DEFAULT_SIGNATURE_HEADER stays working but deprecated with a warning for existing deployments. That directly removes the config creep you flagged.

One scope question: Should this stay signature-only, or give the same treatment to event-id, timestamp, and topic too (deprecating their DISABLE_DEFAULT_*_HEADER flags the same way)?

@alexluong

Copy link
Copy Markdown
Collaborator

Hi @ambroziepaval, thanks for the PR and sorry for the miscommunication/confusion. It was caused by me misunderstanding what @alexbouchardd meant. We connected internally and agreed on the design here. Can you take a look and update the PR accordingly? We can chat more in the issue thread if there's anything you want to discuss further.

…flags

Replace the four DESTINATIONS_WEBHOOK_DISABLE_DEFAULT_*_HEADER flags with four
DESTINATIONS_WEBHOOK_*_HEADER_NAME configs (event-id, signature, timestamp,
topic). Each is three-state: unset uses the default, a value pins the exact
header name, and an empty string disables the header. The deprecated flags keep
working but log a warning at startup.

Tracks hookdeck#982.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ambroziepaval ambroziepaval force-pushed the feat/webhook-signature-header-name branch from 296bdf5 to da870aa Compare July 7, 2026 13:54
@ambroziepaval ambroziepaval changed the title feat(webhook): support custom signature header name feat(webhook): configurable system header names, deprecate DISABLE_* flags Jul 7, 2026

@alexluong alexluong left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great PR! Overall looks great, just got a few small comments here, but otherwise everything makes sense!

Comment on lines +118 to +119
// - name set to a value -> {Name: value} (pin exact name)
// - name set to "" -> {Disabled: true} (disable)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have a scenario of value being a whitespace of some sort " " that we should treat as disabled as well.

For now, as I'm tracing it, it will be treated as default headers instead.

}
}

func TestDestinationWebhookHeaderNames(t *testing.T) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's add the " " case as well as mentioned above

Comment on lines +268 to +272
// Validate the four system headers: reject invalid/reserved pinned names, and
// reject collisions between the effective names (pinned name, or "<prefix>" +
// key) of two enabled headers. A collision would make emission order-dependent
// since Format writes headers from a map, so whichever iterated last would
// silently win.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this blob is better suited at Config.Validate() instead? I know this already runs on startup but this feels like config validation.

That's my intuition but I'm not super super opinionated there. I get that this is destwebhook logic so I can be talked into this as well haha 😅

Comment on lines +282 to +285
| `DESTINATIONS_WEBHOOK_EVENT_ID_HEADER_NAME` | — | Complete name of the event ID header. Unset uses the default `<prefix>event-id`; an explicit value pins that exact name; an empty string disables the header. |
| `DESTINATIONS_WEBHOOK_TIMESTAMP_HEADER_NAME` | — | Complete name of the timestamp header. Unset uses the default `<prefix>timestamp`; an explicit value pins that exact name; an empty string disables the header. |
| `DESTINATIONS_WEBHOOK_TOPIC_HEADER_NAME` | — | Complete name of the topic header. Unset uses the default `<prefix>topic`; an explicit value pins that exact name; an empty string disables the header. |
| `DESTINATIONS_WEBHOOK_SIGNATURE_HEADER_NAME` | — | Complete name of the signature header. Unset uses the default `<prefix>signature`; an explicit value pins that exact name; an empty string disables the header. |

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our OpenAPI schema file, there's ManagedConfig that we should update to include these values as well (and probably remove the DISABLED configs)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants