fix(aso-overlay): rewrap native Response before aws-adapter (fix dev 500)#2839
Open
abhishekgarg18 wants to merge 2 commits into
Open
fix(aso-overlay): rewrap native Response before aws-adapter (fix dev 500)#2839abhishekgarg18 wants to merge 2 commits into
abhishekgarg18 wants to merge 2 commits into
Conversation
…500)
## Symptom
Every GET /config/dev/*/redirects.txt on dev returns HTTP 500 with:
x-error: response.headers.raw is not a function
Verified reproduction with a valid ASO_OVERLAY_API_KEY on multiple tenants
(cm-p117653-e365853, cm-p128729-e2179647). Consistent on both the 200 and
304 conditional-GET code paths. Worked at 2026-07-16T00:03Z (Maksym's
end-to-end verification), broke after v1.655.0 deployed (#2822 EMF
instrumentation).
## Root cause
The error is thrown at `@adobe/helix-universal/aws-adapter.js:254`:
...splitHeaders(response.headers.raw(), con.log),
That helper expects `@adobe/fetch`-style Headers (which has `.raw()`), but
the Response reaching the adapter has native Web-Fetch-API Headers (from
`undici`) whose Headers has no `.raw()`.
Inspection of the produced Lambda bundle
(`dist/spacecat-services/api-service@1.657.0.zip`) shows that esbuild
inlines TWO Response classes side-by-side:
- `Response2` — `@adobe/fetch` shim, has `.raw()` ✅
- `Response6` — `undici` (native fetch spec), no `.raw()` ❌
At the source level `createResponse` (from `@adobe/spacecat-shared-http-utils`)
correctly imports `Response` from `@adobe/fetch` and returns `Response2` in
isolation — verified by a standalone local test. But something in the module
graph after #2822 added `metrics-emf.js` imports to both `RedirectsController`
and `AsoOverlayKeyHandler` now resolves to `Response6` at bundle-time along
one of the code paths.
The exact esbuild module-resolution behavior is opaque; rather than chase
it in the bundle and risk regressing again on any future import graph
change, normalize at the outermost seam so the AWS adapter always sees an
`@adobe/fetch`-compatible Response.
## Fix
New `ensureFetchResponseWrapper` installed as the OUTERMOST wrapper (runs
LAST on the response path, immediately before helix-universal's aws-adapter
serializes the response). On the fast path (Response is already an
`@adobe/fetch` Response), the wrapper is a zero-cost passthrough. On the
slow path (native Response reached us), it rebuilds the response as an
`@adobe/fetch` Response with the same status, headers, and body.
This is defense-in-depth: it fixes the specific interaction from #2822
AND protects against future PRs that could introduce the same shift via
different code paths, without requiring bundle-level bisection every time.
## Why we didn't catch this locally
Two independent gaps that this PR closes:
1. **Unit tests import from source** (`src/controllers/redirects.js`), where
module resolution goes directly to `node_modules/@adobe/fetch`.
`createResponse` returns `Response2` — `.raw()` works. Tests pass.
2. **CI's `bundle-build` step** invokes the bundled Lambda but only against
the `/health` endpoint (JSON, unrelated code path). The overlay path is
not exercised at bundle time.
The rewrap wrapper closes the runtime gap; a follow-up ticket
(SITES-48140 follow-up thread) tracks adding a bundle-level integration
test that hits `/config/dev/*/redirects.txt` against the bundled lambda so
this class of regression is caught pre-merge.
## Test plan
- [x] 7 unit tests on the new wrapper (fast path, slow path, 200/304/404,
empty body, non-object results, error propagation)
- [x] Existing suites still green (redirects.test.js + support/ — 4908
passing across the two directories)
- [x] Lint clean, type-check clean
- [ ] Post-deploy on dev: verify GET /config/dev/cm-p117653-e365853/redirects.txt
returns 200/304/404 as expected (no more 500)
- [ ] Then unblocks Alina's TTL bump verification and the fleet-wide
customer verification (18 envs, task 11)
## Non-goals
Not re-bisecting the exact esbuild module-resolution to identify which
transitive import triggered the shift — that's opaque and time-boxed;
the wrapper defends regardless. If desired, follow-up work can bisect
by re-introducing pieces of #2822's imports one at a time under the
new bundle-integration-test net.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closed
3 tasks
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
This PR will trigger a patch release when merged. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The symptom
Every
GET /config/dev/*/redirects.txton dev is returning HTTP 500 with:```
x-error: response.headers.raw is not a function
```
Reproduced with a valid `ASO_OVERLAY_API_KEY` on multiple tenants (`cm-p117653-e365853`, `cm-p128729-e2179647`). Consistent on both the 200 and 304 conditional-GET code paths. Endpoint was working at 2026-07-16T00:03Z (Maksym's end-to-end verification); broke after v1.655.0 deployed (SITES-48140 EMF instrumentation, #2822).
Root cause
The error is thrown at `@adobe/helix-universal/aws-adapter.js:254`:
```js
...splitHeaders(response.headers.raw(), con.log),
```
That helper expects `@adobe/fetch`-style Headers (which has `.raw()`), but the Response reaching the adapter has native Web-Fetch-API Headers (from `undici`) whose Headers has no `.raw()`.
Bundle inspection (`dist/spacecat-services/api-service@1.657.0.zip`, extracted `index.js`) shows esbuild inlines two Response classes side by side:
At source level, `createResponse` (from `@adobe/spacecat-shared-http-utils`) correctly imports `Response` from `@adobe/fetch` and returns `Response2` — verified by a standalone local test. But after #2822 added `metrics-emf.js` imports to both `RedirectsController` and `AsoOverlayKeyHandler`, the extended module graph now resolves to `Response6` at bundle time along the overlay code path.
Rather than chase the exact esbuild resolution behavior (opaque, fragile, and could regress again on any future import graph change), we normalize at the outermost seam — the wrapper that runs LAST on the response path before the AWS Lambda adapter sees the response.
The fix
New `ensureFetchResponseWrapper` installed as the OUTERMOST wrapper:
This is defense-in-depth: it fixes the specific interaction from #2822 AND protects against any future PR that could introduce the same shift via a different code path.
Zero-regression argument (this is the important part)
The wrapper is intentionally the smallest change that fixes the symptom:
Known limitation, out of scope for this PR: multi-value headers like `Set-Cookie` on the slow path would collapse to their last value. api-service is a token-authenticated API and does not set cookies; if that ever changes, the wrapper's `forEach` loop will need to switch to `getSetCookie()` for that one header per WHATWG fetch spec. Documented inline.
Why we didn't catch this in dev
Two independent gaps:
Follow-up ticket to add a bundle-level integration test that exercises `/config/dev/*/redirects.txt` on the bundled artifact. Tracked as a separate item so this PR stays scoped to the fix.
Test plan
Supersedes revert PR #2838 — closing that in favor of this forward-fix.
Jira: SITES-48140
🤖 Generated with Claude Code