Skip to content

fix: preserve set-cookie headers during SSR redirects#1744

Merged
wattanx merged 2 commits intomainfrom
fix/redirect-response
Mar 6, 2026
Merged

fix: preserve set-cookie headers during SSR redirects#1744
wattanx merged 2 commits intomainfrom
fix/redirect-response

Conversation

@wattanx
Copy link
Collaborator

@wattanx wattanx commented Feb 24, 2026

🔗 Linked issue

Fixes: #1737

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

When useCookie is called before navigateTo in a middleware during SSR, the set-cookie header was not included in the redirect response.

navigateTo called sendRedirect() immediately, which sent the HTTP response before the app:rendered hook (where useCookie writes cookies) could fire.

Changed to use ssrContext['~renderResponse'] to defer the redirect response, the same approach as upstream Nuxt:

  • navigateTo now sets ssrContext['~renderResponse'] instead of calling sendRedirect() directly, deferring the response.
  • The renderer calls the app:rendered hook (which triggers cookie writing) before checking for ~renderResponse.
  • The redirect response is then returned with all accumulated headers including set-cookie.

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

Adopt upstream Nuxt's `ssrContext['~renderResponse']` pattern to defer
redirect responses, ensuring cookies set via useCookie are included in
redirect response headers.
@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 24, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/bridge@1744
npm i https://pkg.pr.new/@nuxt/bridge-schema@1744

commit: 2f0f52b

@wattanx wattanx marked this pull request as ready for review February 24, 2026 14:02
@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

The PR modifies the SSR redirect mechanism to preserve cookies set via useCookie() when navigateTo() triggers a redirect. Instead of directly calling h3's redirect function, the router composable now stores redirect metadata in ssrContext['~renderResponse']. The renderer processes this stored response after emitting the "app:rendered" hook, ensuring cookies are included in response headers before the redirect is applied. URL encoding utilities handle proper formatting of the redirect location. A test page and test case validate that Set-Cookie headers are present in redirect responses.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'fix: preserve set-cookie headers during SSR redirects' accurately summarizes the main change—fixing the loss of Set-Cookie headers when navigateTo triggers a redirect during SSR.
Linked Issues check ✅ Passed All code changes directly address the requirements from issue #1737: the PR implements deferred redirect handling via ssrContext['~renderResponse'] to preserve Set-Cookie headers, updates the router and renderer, and adds a test verifying the fix.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the Set-Cookie header loss during SSR redirects; no out-of-scope modifications are present.
Description check ✅ Passed The pull request description clearly relates to the changeset, explaining the bug fix and implementation approach with sufficient detail.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/redirect-response

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bridge/src/runtime/composables/router.ts`:
- Around line 154-167: The meta-refresh body uses encodedLoc which only replaces
quotes and can allow HTML injection (in function redirect); escape the location
for HTML context before interpolating into the meta tag: add or reuse an
HTML-escape utility (e.g., escapeHtml) and apply it to the location (or
encodedLoc) when building the body string assigned to
nuxtApp.ssrContext!['~renderResponse'].body, while keeping the existing
encodeURL usage for the Location header (encodedHeader) and preserving
sanitizeStatusCode for the statusCode.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ba31ca and 2f0f52b.

📒 Files selected for processing (4)
  • packages/bridge/src/runtime/composables/router.ts
  • packages/bridge/src/runtime/nitro/renderer.ts
  • playground/pages/cookie-with-redirect.vue
  • test/bridge.test.ts

Comment on lines +154 to +167
const isExternalHost = hasProtocol(toPath, { acceptRelative: true })

const redirect = async function (response: any) {
// @ts-expect-error
await nuxtApp.callHook('app:redirected')

await sendRedirect(nuxtApp.ssrContext!.event, location, options?.redirectCode || 302)
const encodedLoc = location.replace(URL_QUOTE_RE, '%22')
const encodedHeader = encodeURL(location, isExternalHost)

nuxtApp.ssrContext!['~renderResponse'] = {
statusCode: sanitizeStatusCode(options?.redirectCode || 302, 302),
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
headers: { location: encodedHeader }
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape the meta‑refresh URL to prevent HTML injection.

Only quotes are escaped today, so a location containing &quot; (or </>) can break the attribute and inject markup. Please HTML‑escape the value used in the meta refresh body.

🛡️ Suggested fix
-        const encodedLoc = location.replace(URL_QUOTE_RE, '%22')
-        const encodedHeader = encodeURL(location, isExternalHost)
+        const encodedHeader = encodeURL(location, isExternalHost)
+        const encodedLoc = encodedHeader
+          .replace(/&/g, '&amp;')
+          .replace(/</g, '&lt;')
+          .replace(/>/g, '&gt;')
+          .replace(/"/g, '&quot;')
 
         nuxtApp.ssrContext!['~renderResponse'] = {
           statusCode: sanitizeStatusCode(options?.redirectCode || 302, 302),
           body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isExternalHost = hasProtocol(toPath, { acceptRelative: true })
const redirect = async function (response: any) {
// @ts-expect-error
await nuxtApp.callHook('app:redirected')
await sendRedirect(nuxtApp.ssrContext!.event, location, options?.redirectCode || 302)
const encodedLoc = location.replace(URL_QUOTE_RE, '%22')
const encodedHeader = encodeURL(location, isExternalHost)
nuxtApp.ssrContext!['~renderResponse'] = {
statusCode: sanitizeStatusCode(options?.redirectCode || 302, 302),
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
headers: { location: encodedHeader }
}
const isExternalHost = hasProtocol(toPath, { acceptRelative: true })
const redirect = async function (response: any) {
// `@ts-expect-error`
await nuxtApp.callHook('app:redirected')
const encodedHeader = encodeURL(location, isExternalHost)
const encodedLoc = encodedHeader
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
nuxtApp.ssrContext!['~renderResponse'] = {
statusCode: sanitizeStatusCode(options?.redirectCode || 302, 302),
body: `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`,
headers: { location: encodedHeader }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bridge/src/runtime/composables/router.ts` around lines 154 - 167,
The meta-refresh body uses encodedLoc which only replaces quotes and can allow
HTML injection (in function redirect); escape the location for HTML context
before interpolating into the meta tag: add or reuse an HTML-escape utility
(e.g., escapeHtml) and apply it to the location (or encodedLoc) when building
the body string assigned to nuxtApp.ssrContext!['~renderResponse'].body, while
keeping the existing encodeURL usage for the Location header (encodedHeader) and
preserving sanitizeStatusCode for the statusCode.

@wattanx
Copy link
Collaborator Author

wattanx commented Feb 25, 2026

I'm planning to release 3.6.2, including this fix. (We were going to release v4, but it's been delayed.)

@wattanx wattanx requested a review from danielroe February 25, 2026 14:11
@wattanx wattanx merged commit 10c492f into main Mar 6, 2026
42 checks passed
@wattanx wattanx deleted the fix/redirect-response branch March 6, 2026 11:55
@github-actions github-actions bot mentioned this pull request Mar 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

useCookie() does not set Set-Cookie header when navigateTo() triggers redirect during SSR

1 participant