> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# Migrating server source ingest and body handling

Server sources now normalize their platform's request into one shared [request scope](https://www.walkeros.io/docs/sources/scope.md) before mapping, and accept one shared [event envelope](https://www.walkeros.io/docs/sources/envelope.md). 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[​](#configingest-paths "Direct link to configingest-paths")

### Express[​](#express "Direct link to Express")

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

### AWS Lambda[​](#aws-lambda "Direct link to 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.

| Before                                    | After                                                                                   |
| ----------------------------------------- | --------------------------------------------------------------------------------------- |
| `requestContext.*`                        | `raw.requestContext.*`, or use the contract field: `ip` covers both versions' source IP |
| `httpMethod`, `rawPath`, `rawQueryString` | `method`, `path`, `query.*`                                                             |
| `isBase64Encoded`                         | no longer needed: `body` arrives decoded and parsed                                     |

### Fetch[​](#fetch "Direct link to 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.

| Before                                  | After                                                                  |
| --------------------------------------- | ---------------------------------------------------------------------- |
| `{ fn: (req) => req.headers.get('x') }` | `{ key: 'headers.x' }`                                                 |
| any `{ fn }` receiving the `Request`    | the function now receives the scope; use `scope.raw` for the `Request` |

### Cloud Functions[​](#cloud-functions "Direct link to Cloud Functions")

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

## Request bodies arrive parsed[​](#request-bodies-arrive-parsed "Direct link to 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[​](#a-non-event-object-body-is-forwarded-not-replaced "Direct link to 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[​](#new-batches-on-every-source "Direct link to 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](https://www.walkeros.io/docs/sources/envelope.md): `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[​](#new-cloud-functions-serves-a-get-pixel "Direct link to 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[​](#removed-the--events-----body "Direct link to 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": [ ... ] }`.
