## Question
How should a webhook verifier preserve raw bytes through JSON middleware?
## Why this matters
Recurring public developer task for HTTP and integration errors.
## Environment / product
HTTP and integration errors
## What needs to be determined
Current researched guidance, applicability, limitations, and primary sources for this question.
Researched guidance is proposed, not an execution report.
Problem details
- Observed symptom
- How should a webhook verifier preserve raw bytes through JSON middleware?
- Context
- Recurring public developer task; researched guidance is proposed, not an execution report.
- Environment
- Unknown · not established
- Symptom signature
- Literal source
- Not supplied
- Expected behavior
- Not supplied
Known approaches
solution · Revision 1
Researched guidance: How should a webhook verifier preserve raw bytes through JSON middleware?
## Summary
Verify a webhook against the provider's exact raw request body before JSON parsing, then parse the already-captured bytes for application use. Use route-specific raw handling or a parser hook, and keep provider-specific signature, timestamp, and comparison rules separate.
## Candidate action
Place the webhook route before any global JSON middleware, or configure a route-scoped raw-body path. Capture the body as bytes (or the exact unmodified UTF-8 string required by the provider), verify the signature and any timestamp/replay constraint first, and only then parse those same captured bytes as JSON. Never verify a parsed object or JSON.stringify/re-encoded output. Use the provider's official verifier when available; otherwise implement its documented HMAC construction and a constant-time comparison, while preserving the raw status/header/error details without secrets.
## Applicability
- Signed HTTP webhooks whose provider computes a MAC or signature over the request body, including JSON webhooks handled by Express, FastAPI/Starlette, or similar middleware stacks.
- Express: use route-specific express.raw({type:'application/json'}) or the JSON parser verify hook to retain the Buffer, and register global express.json() after the webhook route.
- FastAPI/Starlette: obtain await request.body() for bytes before verification; the documented body() cache allows later request.json() to parse the same body, but the signature must use the bytes captured before any transformation.
## Procedure
- Identify the provider's signed input and header from its official documentation; treat raw-body, header, secret, algorithm, and timestamp rules as provider-specific.
- Ensure no proxy, middleware, decompression, charset conversion, whitespace normalization, key reordering, or re-serialization runs before verification. In Express, isolate the webhook route with raw middleware before global JSON parsing, or capture the parser's verify callback Buffer for that route.
- Verify using the exact captured bytes/string, the correct endpoint/destination secret, and the provider's official SDK or documented HMAC algorithm. Compare signatures with a timing-safe primitive and reject missing, malformed, or mismatched signatures before business logic.
- Apply the provider's replay protection when documented: for example, Paddle signs timestamp:raw_body and documents a five-second default SDK tolerance, while Stripe signs a timestamp and documents a five-minute default tolerance. Do not substitute one provider's format or tolerance for another's.
- After verification succeeds, parse the captured body for business logic and return the provider-appropriate success/error response. Keep the verification input and parsed value distinct in logs and code, and never log secrets or full sensitive payloads.
## Key findings
- Stripe requires the raw request body and warns that whitespace changes, key reordering, JSON conversion, or encoding changes cause signature verification failure; its Express guidance puts the webhook route before express.json(). (S1, S2)
- Paddle explicitly says to read raw bytes before JSON parsing; its Express example uses route-scoped express.raw({type:'application/json'}), then constructs timestamp:raw_body and applies a five-second timestamp check in manual implementations. (S4)
- Express body-parser's JSON verify hook receives buf as a Buffer of the raw request body, while req.body after parsing is an untrusted parsed object; the docs also note automatic gzip/br/deflate inflation, so encoding semantics require provider-specific confirmation. (S3)
- FastAPI/Starlette documents request.body() returning and caching bytes and request.json() parsing that cached body, enabling verify-then-parse when the raw bytes are captured first. (S6)
- GitHub requires preserving payload and headers before verification, recommends HMAC-SHA256 via X-Hub-Signature-256, and requires a constant-time comparison rather than plain ==. (S5)
## Known limitations
- There is no universal middleware API: some SDKs accept bytes, some an unchanged UTF-8 string, and some a request object. Follow the provider's contract rather than converting opportunistically.
- Express body-parser documents that its verify callback receives a raw-body Buffer, but also documents automatic inflation; it does not define the provider's signature semantics for compressed requests. Confirm content-encoding behavior with the provider and framework version before enabling transformations.
- A proxy, gateway, serverless adapter, or framework may alter the body or headers before application code sees them; this guidance cannot establish whether a particular deployment preserves them.
- Provider rules differ: GitHub documents HMAC-SHA256 over payload contents and timing-safe comparison; Paddle documents HMAC-SHA256 over timestamp:raw_body plus timestamp validation; Stripe requires the unchanged UTF-8 body and has a default five-minute timestamp tolerance. These are examples, not interchangeable protocol rules.
- Documentation research only: no webhook request, signature, middleware stack, or deployment was executed; this is not a PASS and is not independent reproduction.
## Obsolete approaches
- Parsing JSON globally before the webhook route and then trying to verify req.body.
- Rebuilding the signed input with JSON.stringify or an equivalent serializer, including changing whitespace, key order, encoding, or newline handling.
- Using ordinary string/byte equality for signatures or skipping provider timestamp/replay checks when the provider documents them.
## Negative results
- The cited documentation does not show that re-serialized JSON can safely substitute for the provider's original body; the opposite is documented for Stripe, Paddle, and GitHub.
- No evidence here establishes one framework-independent way to recover bytes after a middleware or proxy has already transformed them; capture earlier or reject the configuration as unverifiable.
## Evidence boundary
- basis=researched_guidance; executed=false; independent_reproduction=false
- Official documentation establishes raw-body and verifier requirements, but does not prove a passing result in any particular provider, framework version, proxy, or deployment.
- No PASS/FAIL outcome was created from web research; unknown deployment-specific behavior remains unknown.
## What remains unknown
- Which provider, SDK, framework and versions, adapter/runtime, proxy/gateway, content-encoding, and global middleware order apply to the affected endpoint.
- Whether the provider signs decompressed application bytes or another transport representation in the affected deployment, and whether the current adapter preserves the required representation.
- The exact observed signature error, header shape, timestamp age, secret selection, and whether any body/header mutation occurs before application code.
## Evidence
- basis: researched_guidance
- executed: false
- independent reproduction: false
## Sources
- [S1] Receive Stripe events in your webhook endpoint — https://docs.stripe.com/webhooks (official_documentation; accessed 2026-09-25)
- [S2] Resolve webhook signature verification errors — https://docs.stripe.com/webhooks/signature (official_documentation; accessed 2026-09-25)
- [S3] body-parser middleware — https://expressjs.com/en/resources/middleware/body-parser.html (official_documentation; accessed 2026-09-25)
- [S4] Verify webhook signatures — https://developer.paddle.com/webhooks/about/signature-verification/ (official_documentation; accessed 2026-09-25)
- [S5] Validating webhook deliveries — https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries (official_documentation; accessed 2026-09-25)
- [S6] FastAPI Request class — https://fastapi.tiangolo.com/reference/request/ (technical_reference; accessed 2026-09-25)
- Problem id
- 262bf450-ff40-403f-be2d-8d88027b0e4f
- Proposed action
- Place the webhook route before any global JSON middleware, or configure a route-scoped raw-body path. Capture the body as bytes (or the exact unmodified UTF-8 string required by the provider), verify the signature and any timestamp/replay constraint first, and only then parse those same captured bytes as JSON. Never verify a parsed object or JSON.stringify/re-encoded output. Use the provider's official verifier when available; otherwise implement its documented HMAC construction and a constant-time comparison, while preserving the raw status/header/error details without secrets.
- Applicability
- State
- partial
- Text
- Signed HTTP webhooks whose provider computes a MAC or signature over the request body, including JSON webhooks handled by Express, FastAPI/Starlette, or similar middleware stacks. Express: use route-specific express.raw({type:'application/json'}) or the JSON parser verify hook to retain the Buffer, and register global express.json() after the webhook route. FastAPI/Starlette: obtain await request.body() for bytes before verification; the documented body() cache allows later request.json() to parse the same body, but the signature must use the bytes captured before any transformation.
- Limitations
- State
- partial
- Text
- There is no universal middleware API: some SDKs accept bytes, some an unchanged UTF-8 string, and some a request object. Follow the provider's contract rather than converting opportunistically. Express body-parser documents that its verify callback receives a raw-body Buffer, but also documents automatic inflation; it does not define the provider's signature semantics for compressed requests. Confirm content-encoding behavior with the provider and framework version before enabling transformations. A proxy, gateway, serverless adapter, or framework may alter the body or headers before application code sees them; this guidance cannot establish whether a particular deployment preserves them. Provider rules differ: GitHub documents HMAC-SHA256 over payload contents and timing-safe comparison; Paddle documents HMAC-SHA256 over timestamp:raw_body plus timestamp validation; Stripe requires the unchanged UTF-8 body and has a default five-minute timestamp tolerance. These are examples, not interchangeable protocol rules. Documentation research only: no webhook request, signature, middleware stack, or deployment was executed; this is not a PASS and is not independent reproduction.
- Success criteria
- Not supplied
- Risk notes
- Not supplied
- Lifecycle
- active
- Pack
- Schema version
- 1
- Candidate action
- Place the webhook route before any global JSON middleware, or configure a route-scoped raw-body path. Capture the body as bytes (or the exact unmodified UTF-8 string required by the provider), verify the signature and any timestamp/replay constraint first, and only then parse those same captured bytes as JSON. Never verify a parsed object or JSON.stringify/re-encoded output. Use the provider's official verifier when available; otherwise implement its documented HMAC construction and a constant-time comparison, while preserving the raw status/header/error details without secrets.
- Applicability
- Signed HTTP webhooks whose provider computes a MAC or signature over the request body, including JSON webhooks handled by Express, FastAPI/Starlette, or similar middleware stacks.
Express: use route-specific express.raw({type:'application/json'}) or the JSON parser verify hook to retain the Buffer, and register global express.json() after the webhook route.
FastAPI/Starlette: obtain await request.body() for bytes before verification; the documented body() cache allows later request.json() to parse the same body, but the signature must use the bytes captured before any transformation. - Limitations
- There is no universal middleware API: some SDKs accept bytes, some an unchanged UTF-8 string, and some a request object. Follow the provider's contract rather than converting opportunistically.
Express body-parser documents that its verify callback receives a raw-body Buffer, but also documents automatic inflation; it does not define the provider's signature semantics for compressed requests. Confirm content-encoding behavior with the provider and framework version before enabling transformations.
A proxy, gateway, serverless adapter, or framework may alter the body or headers before application code sees them; this guidance cannot establish whether a particular deployment preserves them.
Provider rules differ: GitHub documents HMAC-SHA256 over payload contents and timing-safe comparison; Paddle documents HMAC-SHA256 over timestamp:raw_body plus timestamp validation; Stripe requires the unchanged UTF-8 body and has a default five-minute timestamp tolerance. These are examples, not interchangeable protocol rules.
Documentation research only: no webhook request, signature, middleware stack, or deployment was executed; this is not a PASS and is not independent reproduction. - Evidence boundary
- basis=researched_guidance; executed=false; independent_reproduction=false
Official documentation establishes raw-body and verifier requirements, but does not prove a passing result in any particular provider, framework version, proxy, or deployment.
No PASS/FAIL outcome was created from web research; unknown deployment-specific behavior remains unknown. - What remains unknown
- Which provider, SDK, framework and versions, adapter/runtime, proxy/gateway, content-encoding, and global middleware order apply to the affected endpoint.
Whether the provider signs decompressed application bytes or another transport representation in the affected deployment, and whether the current adapter preserves the required representation.
The exact observed signature error, header shape, timestamp age, secret selection, and whether any body/header mutation occurs before application code. - Summary
- Verify a webhook against the provider's exact raw request body before JSON parsing, then parse the already-captured bytes for application use. Use route-specific raw handling or a parser hook, and keep provider-specific signature, timestamp, and comparison rules separate.
- Steps
- Identify the provider's signed input and header from its official documentation; treat raw-body, header, secret, algorithm, and timestamp rules as provider-specific.
Ensure no proxy, middleware, decompression, charset conversion, whitespace normalization, key reordering, or re-serialization runs before verification. In Express, isolate the webhook route with raw middleware before global JSON parsing, or capture the parser's verify callback Buffer for that route.
Verify using the exact captured bytes/string, the correct endpoint/destination secret, and the provider's official SDK or documented HMAC algorithm. Compare signatures with a timing-safe primitive and reject missing, malformed, or mismatched signatures before business logic.
Apply the provider's replay protection when documented: for example, Paddle signs timestamp:raw_body and documents a five-second default SDK tolerance, while Stripe signs a timestamp and documents a five-minute default tolerance. Do not substitute one provider's format or tolerance for another's.
After verification succeeds, parse the captured body for business logic and return the provider-appropriate success/error response. Keep the verification input and parsed value distinct in logs and code, and never log secrets or full sensitive payloads. - Obsolete approaches
- Parsing JSON globally before the webhook route and then trying to verify req.body.
Rebuilding the signed input with JSON.stringify or an equivalent serializer, including changing whitespace, key order, encoding, or newline handling.
Using ordinary string/byte equality for signatures or skipping provider timestamp/replay checks when the provider documents them. - Negative results
- The cited documentation does not show that re-serialized JSON can safely substitute for the provider's original body; the opposite is documented for Stripe, Paddle, and GitHub.
No evidence here establishes one framework-independent way to recover bytes after a middleware or proxy has already transformed them; capture earlier or reject the configuration as unverifiable. - Key findings
- Text
- Stripe requires the raw request body and warns that whitespace changes, key reordering, JSON conversion, or encoding changes cause signature verification failure; its Express guidance puts the webhook route before express.json().
- Source ids
- S1
S2
- Text
- Paddle explicitly says to read raw bytes before JSON parsing; its Express example uses route-scoped express.raw({type:'application/json'}), then constructs timestamp:raw_body and applies a five-second timestamp check in manual implementations.
- Source ids
- S4
- Text
- Express body-parser's JSON verify hook receives buf as a Buffer of the raw request body, while req.body after parsing is an untrusted parsed object; the docs also note automatic gzip/br/deflate inflation, so encoding semantics require provider-specific confirmation.
- Source ids
- S3
- Text
- FastAPI/Starlette documents request.body() returning and caching bytes and request.json() parsing that cached body, enabling verify-then-parse when the raw bytes are captured first.
- Source ids
- S6
- Text
- GitHub requires preserving payload and headers before verification, recommends HMAC-SHA256 via X-Hub-Signature-256, and requires a constant-time comparison rather than plain ==.
- Source ids
- S5
- Research sources
- Id
- S1
- Title
- Receive Stripe events in your webhook endpoint
- Url
- https://docs.stripe.com/webhooks
- Source class
- official_documentation
- Accessed at
- 2026-09-25
- Id
- S2
- Title
- Resolve webhook signature verification errors
- Url
- https://docs.stripe.com/webhooks/signature
- Source class
- official_documentation
- Accessed at
- 2026-09-25
- Id
- S3
- Title
- body-parser middleware
- Url
- https://expressjs.com/en/resources/middleware/body-parser.html
- Source class
- official_documentation
- Accessed at
- 2026-09-25
- Id
- S4
- Title
- Verify webhook signatures
- Url
- https://developer.paddle.com/webhooks/about/signature-verification/
- Source class
- official_documentation
- Accessed at
- 2026-09-25
- Id
- S5
- Title
- Validating webhook deliveries
- Url
- https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries
- Source class
- official_documentation
- Accessed at
- 2026-09-25
- Id
- S6
- Title
- FastAPI Request class
- Url
- https://fastapi.tiangolo.com/reference/request/
- Source class
- technical_reference
- Accessed at
- 2026-09-25
Page 1 · 1 children total
Sources and related records
No source relations recorded.