malformed_signature_header
hooksentinel error malformed_signature_header — the signature header was present but didn't parse into the format the provider adapter expects.
Summary
| Field | Value |
|---|---|
| Code | malformed_signature_header |
| HTTP status | 400 |
| Retryable | No |
What caused it
The expected header was present, but its contents didn't parse into the shape the provider adapter expects. For example, Stripe's Stripe-Signature header is expected to look like t=1614556800,v1=5257a869e...; if it's missing the t= or v1= component, isn't comma-separated correctly, or the v1 value isn't valid hex, verification can't even attempt a comparison and hooksentinel throws malformed_signature_header rather than treating it as a plain mismatch.
Unlike invalid_signature, this means the header's shape is wrong, not that a correctly-shaped signature failed to match.
Common causes:
- A proxy or load balancer merging duplicate headers. If multiple
Stripe-Signatureheaders are sent (which can legitimately happen when Stripe rotates signing secrets and sends both old and new signatures during the transition), some proxies join them with a comma into a single malformed value instead of preserving them as separate values. - A header size limit truncating the value. Some infrastructure enforces a per-header size cap (e.g. 4–8 KB) that's rarely hit but can truncate an unusually long signature header, especially for providers that include several signatures at once.
- Manual construction of the header in a test or debugging script that doesn't match the provider's actual format — see Testing for
createTestSigner, which produces a correctly formatted header for you instead.
The fix
Log the raw header value in your error handler to see exactly what arrived:
import { createWebhookHandler, stripe } from '@hooksentinel/core';
const handler = createWebhookHandler({
provider: stripe({ secret: process.env.STRIPE_WEBHOOK_SECRET! }),
onEvent: async (event) => { /* ... */ },
onError: async (error, ctx) => {
if (error.code === 'malformed_signature_header') {
logger.warn('malformed signature header', {
provider: ctx.provider,
header: ctx.headers['stripe-signature'],
});
}
},
});Check for proxy/load-balancer header merging if the logged value looks like two valid signatures joined with a comma in an unexpected place — configure the proxy to forward the header as-is, or to preserve multiple header instances rather than collapsing them.
If using a test script, switch to createTestSigner from @hooksentinel/core/testing rather than hand-constructing the header string — it produces exactly the format each provider adapter expects, including during a secret rotation window.
Last updated on
missing_signature_header
hooksentinel error missing_signature_header — the request didn't include the header the provider uses to sign webhooks, such as Stripe-Signature or X-Hub-Signature-256.
timestamp_out_of_tolerance
hooksentinel error timestamp_out_of_tolerance — the request's signed timestamp is outside the allowed window, usually clock skew or a replayed request.