hooksentinel
Errors

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

FieldValue
Codemalformed_signature_header
HTTP status400
RetryableNo

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-Signature headers 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

On this page