Skip to main content

Migrating server source ingest and body handling

Server sources now normalize their platform's request into one shared request scope before mapping, and accept one shared event envelope. A config.ingest mapping written once resolves the same on Express, Cloud Functions, Lambda and Fetch, and the same POST body works everywhere.

Most changes are additive. The breaking ones are listed below with exact replacements.

config.ingest paths

Express

BeforeAfter
ip, method, path, body, headers.*unchanged
urlnow an absolute URL, or '' without a host header. For the old path+query value use raw.url, or map path
protocoluse headers.x-forwarded-proto or raw.protocol
hostnameuse headers.host or raw.hostname
query.*flat strings; repeated keys join with ,. Nested and array query parsing stays on raw.query

AWS Lambda

Header lookups are now case-insensitive and repeated headers are joined rather than dropped, on both API Gateway versions. The two gateway versions produce an identical scope.

BeforeAfter
requestContext.*raw.requestContext.*, or use the contract field: ip covers both versions' source IP
httpMethod, rawPath, rawQueryStringmethod, path, query.*
isBase64Encodedno longer needed: body arrives decoded and parsed

Fetch

{ key: ... } mappings never resolved on this source, because a WHATWG Request and its Headers are class instances that mapping paths cannot descend. They now work.

BeforeAfter
{ fn: (req) => req.headers.get('x') }{ key: 'headers.x' }
any { fn } receiving the Requestthe function now receives the scope; use scope.raw for the Request

Cloud Functions

method, headers.* and ip are unchanged. body now arrives parsed.

Request bodies arrive parsed

On Lambda and Cloud Functions the body is parsed once, when the scope is built. A sendBeacon payload sent as text/plain and a base64 encoded API Gateway body both resolve before mapping, so mapping and the event pipeline now read the same normalized input. They previously disagreed, because the parse ran after the scope was built.

A body that does not parse to an object stays raw in ingest.body and produces one empty event, which is what lets a source.before transformer decode it. There, ingest.body and the event deliberately differ.

A non-event object body is forwarded, not replaced

On Lambda and Cloud Functions, a POST body that was an object but not a valid event used to push an empty event. It is now forwarded verbatim, so a source.before transformer receives the whole payload and can rewrite it into an event. ingest.body is unchanged, so a flow that decoded the raw body from there keeps working; the event the before chain sees is simply richer.

New: batches on every source

{ "batch": [ ... ] } and a bare top-level array are now accepted by all four sources, capped by settings.maxBatchSize (default 100). This is additive: these bodies previously answered 400.

Note the current limit documented on the envelope page: ingest is request scoped, so with @walkeros/transformer-bot or @walkeros/transformer-validate in the chain, per event annotations collapse to the last event of a batch.

New: Cloud Functions serves a GET pixel

sourceCloudFunction previously answered 405 to GET. It now serves the tracking pixel like the other three sources. Set settings.enablePixelTracking: false to restore the 405.

Removed: the { "events": [ ... ] } body

The Cloud Functions page documented a { "events": [ ... ] } batch body and a settings.batch flag. Neither was ever implemented, so no working deployment can depend on them. Use { "batch": [ ... ] }.

💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)