Skip to content

feat(vtex)!: forward Set-Cookie through cart-adjacent actions#56

Merged
vibe-dex merged 1 commit into
mainfrom
fix/checkout-set-cookie-propagation
May 21, 2026
Merged

feat(vtex)!: forward Set-Cookie through cart-adjacent actions#56
vibe-dex merged 1 commit into
mainfrom
fix/checkout-set-cookie-propagation

Conversation

@vibe-dex
Copy link
Copy Markdown
Contributor

@vibe-dex vibe-dex commented May 21, 2026

Summary

Closes the framework-level VTEX Set-Cookie propagation gap for sites consuming @decocms/apps via createServerFn action handlers. Three coupled fixes that have to ship together.

The bug class

@decocms/apps/vtex/client.ts exposes two fetch helpers:

  • vtexFetch — plain VTEX API call, no RequestContext interaction.
  • vtexFetchWithCookies — captures upstream Set-Cookie headers onto RequestContext.responseHeaders, expects the host framework to forward them onto the actual HTTP response.

The cookies that VTEX sets in response to cart-adjacent calls (checkout.vtex.com, CheckoutOrderFormOwnership, segment cookies) need to reach the browser to keep the storefront's local orderForm reference and VTEX's server-side orderForm bound to the same orderFormId. Two independent gaps were stopping that.

Hole A — cart-adjacent actions bypassed the cookie-capture helper

Seven actions hit /api/checkout/... endpoints but used the plain vtexFetch, leaving any Set-Cookie VTEX returned unobserved. The downstream forwardResponseCookies bridge had nothing to forward for these endpoints. Switched to vtexFetchWithCookies:

Action Endpoint Why it matters
simulateCart POST /api/checkout/pub/orderForms/simulation Orderform-scoped; can rotate segment cookie.
setShippingPostalCode POST /api/checkout/pub/orderForm/{id}/attachments/shippingData VTEX docs say this endpoint can rotate CheckoutOrderFormOwnership.
getInstallments GET /api/checkout/pub/orderForm/{id}/installments OrderForm-scoped; preserves session cookies.
updateItemPrice PUT /api/checkout/pub/orderForm/{id}/items/{idx}/price Mutates the orderForm.
changeToAnonymousUser GET /api/checkout/changeToAnonymousUser/{id} Rotates ownership cookies by design.
clearOrderFormMessages POST /api/checkout/pub/orderForm/{id}/messages/clear OrderForm-scoped.
getSellersByRegion GET /api/checkout/pub/regions/ Defensive — /api/checkout/* family is cookie-aware.

Hole B — Headers-init silently collapsed in vtexFetchWithCookies

The function cast init.headers to Record<string, string> and spread it into a new object to inject the auto-forwarded Cookie:

```ts
init = { ...init, headers: { ...existingHeaders, cookie: cookies } };
```

When the caller passes a Headers instance (which createVtexCheckoutProxy does in vtexFetchResponse), spreading it collapses to {} — wiping every other header the caller had set (auth, content-type, trace context). Same failure mode that PR #53 fixed for vtexFetchResponse/vtexCachedFetch; this PR closes the same hole in vtexFetchWithCookies.

Replaced with a withCookieHeader(headers, cookieValue) helper backed by the Headers constructor — absorbs every HeadersInit shape (Headers, string[][], Record) correctly.

Hole C — generator contract was deleted, sites can no longer regen

vtex/invoke.ts was deleted on 2026-03-30 (commit 0303cbb) on the assumption that setupApps() would auto-register handlers from the manifest. But @decocms/start/scripts/generate-invoke.ts still scans this file as its source of truth, so sites running npm run generate:invoke against any apps version >= 1.15.0 hit a hard invoke.ts not found error.

Restored at the current action shapes:

  • single-props-object call convention (matches every action signature in vtex/actions/*)
  • no VtexFetchResult unwrapping (actions return data directly since 0303cbb)
  • all 17 actions previously exposed are re-exposed here.

Changes

  • vtex/actions/checkout.ts — 7 cart-adjacent actions switched to vtexFetchWithCookies; drop the now-unused vtexFetch import.
  • vtex/client.ts — new readCookieHeader / withCookieHeader helpers; vtexFetchWithCookies refactored to funnel every header-merge through Headers.
  • vtex/invoke.ts — restored generator contract, 17 actions, current props-object call shape.
  • vtex/__tests__/client-set-cookie-forward.test.ts — 6 new regression tests:
    • inbound Set-Cookie capture into RequestContext.responseHeaders
    • Domain= stripping (browser scope correction)
    • skips vtex_is_session / vtex_is_anonymous (managed by middleware, not actions)
    • safe outside RequestContext
    • Headers-init preservation of other caller headers (Hole B)
    • sanitises caller-supplied Cookie when passed via Headers

Coordination

Paired with decocms/deco-start#195 (matching major) that emits a forwardResponseCookies() bridge in invoke.gen.ts. Sites get the full fix after bumping both packages and re-running npm run generate:invoke.

Test plan

  • bun run test — 34 files, 424/424 pass
  • bun run typecheck — clean
  • bun run lint — 0 errors (pre-existing warnings only)
  • End-to-end generator smoke test: @decocms/start@HEAD generator run against this branch produces a valid invoke.gen.ts with forwardResponseCookies() calls on every handler.

BREAKING CHANGE

Requires the matching @decocms/start major containing the forwardResponseCookies() emit. Sites bumping @decocms/apps alone gain the per-action cookie capture but won't propagate Set-Cookies to the browser until they also bump @decocms/start and regenerate src/server/invoke.gen.ts.

Made with Cursor


Summary by cubic

Forward VTEX Set-Cookie headers through cart-adjacent actions so checkout cookies reach the browser and the cart stays in sync. Fixes header merging in vtexFetchWithCookies and restores the vtex/invoke.ts generator contract.

  • Bug Fixes

    • Switched 7 /api/checkout/* actions to vtexFetchWithCookies: simulateCart, setShippingPostalCode, getInstallments, updateItemPrice, changeToAnonymousUser, clearOrderFormMessages, getSellersByRegion.
    • Fixed cookie/header handling in vtexFetchWithCookies using readCookieHeader and withCookieHeader to preserve all caller headers and sanitize cookies across all HeadersInit shapes.
    • Restored vtex/invoke.ts so npm run generate:invoke works again and re-exposes 17 actions; added regression tests for Set-Cookie capture, domain stripping, skipping IS cookies, and header preservation.
  • Migration

    • Breaking: requires the matching @decocms/start major that emits forwardResponseCookies() in generated handlers.
    • Steps: upgrade @decocms/apps and @decocms/start, then run npm run generate:invoke.

Written for commit 9ffefb3. Summary will update on new commits. Review in cubic

Closes the framework-level VTEX Set-Cookie propagation gap for sites
consuming `@decocms/apps` via `createServerFn` action handlers. VTEX's
`checkout.vtex.com` and `CheckoutOrderFormOwnership` cookies were
silently dropped on the action path, letting the storefront's local
orderForm reference drift away from VTEX's server-side orderForm.

Three fixes in one PR:

1. Cart-adjacent actions switched to `vtexFetchWithCookies`. Seven
   `/api/checkout/...` actions previously used the plain `vtexFetch`
   helper, which does not capture VTEX's `Set-Cookie` headers onto
   `RequestContext.responseHeaders`. The bridge in generated
   `invoke.gen.ts` had nothing to forward. Updated:
   `simulateCart`, `setShippingPostalCode` (per VTEX docs, can rotate
   the ownership cookie), `getInstallments`, `updateItemPrice`,
   `changeToAnonymousUser`, `clearOrderFormMessages`,
   `getSellersByRegion`.

2. Headers-aware merge in `vtexFetchWithCookies`. The previous
   implementation cast `init.headers` to `Record<string, string>` and
   spread it into a new object to inject the auto-forwarded `Cookie`
   header. When the caller passed a `Headers` instance, the spread
   collapsed to `{}` and silently wiped every other header the caller
   had set (auth, content-type, trace context). Replaced with a
   `withCookieHeader(headers, cookieValue)` helper backed by the
   `Headers` constructor — the same Headers-aware fix that landed in
   #53 for `vtexFetchResponse`, applied here for completeness.

3. Restore `vtex/invoke.ts` as the generator contract. The file was
   deleted on 2026-03-30 (commit 0303cbb) on the assumption that
   `setupApps()` would auto-register handlers from the manifest. But
   the framework's `@decocms/start/scripts/generate-invoke.ts` still
   scans this file as its source of truth, so sites running
   `npm run generate:invoke` against any apps version >= 1.15.0 hit a
   hard "invoke.ts not found" error. Restored at the current action
   shapes (single-props-object call convention, no `VtexFetchResult`
   unwrapping). All 17 actions previously exposed are re-exposed here.

Paired with a matching `@decocms/start` major that emits a
`forwardResponseCookies()` bridge in `invoke.gen.ts`. Sites get the
full fix after bumping both packages and re-running
`npm run generate:invoke`.

BREAKING CHANGE: requires the matching `@decocms/start` major
containing the `forwardResponseCookies()` emit. Sites bumping
`@decocms/apps` alone gain the per-action cookie capture but won't
propagate Set-Cookies to the browser until they also bump
`@decocms/start` and regenerate `src/server/invoke.gen.ts`.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vibe-dex vibe-dex requested a review from a team May 21, 2026 21:00
@vibe-dex vibe-dex merged commit 33eddd2 into main May 21, 2026
1 of 2 checks passed
@github-actions
Copy link
Copy Markdown

🎉 This PR is included in version 2.0.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant