Skip to content

Conversation

@onmax
Copy link
Contributor

@onmax onmax commented Oct 12, 2025

Fixes #33273 where navigateTo would drop percent-encoding for internal redirects triggered from middleware on the server.

Summary

  • normalize both the middleware target and the resolved route via encodeURL before comparing them
  • cache the encoded target inside the afterEach hook to avoid reparsing on every navigation check
  • add a regression fixture + assertion so redirects with encoded query params keep their encoded values

Testing

  • pnpm vitest run test/basic.test.ts

@onmax onmax requested a review from danielroe as a code owner October 12, 2025 15:22
@bolt-new-by-stackblitz
Copy link

Review PR in StackBlitz Codeflow Run & review this pull request in StackBlitz Codeflow.

@coderabbitai
Copy link

coderabbitai bot commented Oct 12, 2025

Caution

Review failed

Failed to post review comments

Walkthrough

The router composable was changed: encodeHash and encodePath were imported and encodeURL was reworked to parse locations with parseURL, normalise and encode pathname, re-encode query keys/values via parseQuery, and encode the hash. encodeURL now returns a reconstructed encoded representation for internal and external URLs. navigateTo middleware redirect detection now compares encoded forms (encodeURL(final.fullPath) === encodeURL(fullPath)). Tests and fixtures were added/updated: a redirect-preserves-encoded-query test and pages navigate-to-encoded-query.vue and redirect-target.vue; several encodeURL expectations and bundle size snapshots were updated.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Core change in URL encoding logic with moderate complexity
  • Small conditional change in redirect handling
  • Test additions and snapshot updates; mixed file types and reasoning required

Pre-merge checks and finishing touches and finishing touches

✅ Passed checks (5 passed)
Check name Status Explanation
Title Check ✅ Passed The title clearly summarises the main fix by indicating that percent-encoding is preserved in navigateTo, accurately reflecting the core change to the encodeURL comparison in middleware redirection.
Linked Issues Check ✅ Passed The implementation updates navigateTo to compare encoded URLs using encodeURL for both the target and the resolved route, adds caching in the afterEach hook, and introduces regression fixtures and assertions, fully addressing the linked issue’s requirement to preserve percent-encoding in query parameters.
Out of Scope Changes Check ✅ Passed All code changes and test adjustments focus on preserving percent-encoding in navigateTo or updating tests to reflect the modified encodeURL logic, and the bundle size snapshot updates merely align expected sizes after the refactor rather than introduce unrelated functionality.
Description Check ✅ Passed The description directly addresses fixing issue #33273, outlines the use of encodeURL for normalization, describes caching optimisations, and mentions the added regression tests, all of which align with the pull request changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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: 0

🧹 Nitpick comments (2)
test/fixtures/basic/app/pages/redirect-target.vue (1)

5-7: Consider adding TypeScript to script setup.

The coding guidelines specify using <script setup lang="ts"> for Vue components. Adding TypeScript would provide type safety and align with the project's conventions.

Apply this diff:

-<script setup>
+<script setup lang="ts">
 const route = useRoute()
 </script>

Based on coding guidelines.

test/fixtures/basic/app/pages/navigate-to-encoded-query.vue (1)

5-9: Consider adding TypeScript to script setup.

The coding guidelines specify using <script setup lang="ts"> for Vue components. Adding TypeScript would provide type safety and align with the project's conventions.

Apply this diff:

-<script setup>
+<script setup lang="ts">
 // This should trigger a redirect to /redirect-target?callback=/other
 // The query parameter contains a path that should be preserved
 await navigateTo('/redirect-target?callback=/other', { redirectCode: 302 })
 </script>

Based on coding guidelines.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf102e8 and 3852c90.

📒 Files selected for processing (4)
  • packages/nuxt/src/app/composables/router.ts (1 hunks)
  • test/basic.test.ts (1 hunks)
  • test/fixtures/basic/app/pages/navigate-to-encoded-query.vue (1 hunks)
  • test/fixtures/basic/app/pages/redirect-target.vue (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.vue

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use <script setup lang="ts"> and the composition API when creating Vue components

Files:

  • test/fixtures/basic/app/pages/redirect-target.vue
  • test/fixtures/basic/app/pages/navigate-to-encoded-query.vue
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/app/composables/router.ts
  • test/basic.test.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Write unit tests for core functionality using vitest

Files:

  • test/basic.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: build
  • GitHub Check: code
🔇 Additional comments (2)
packages/nuxt/src/app/composables/router.ts (1)

217-217: LGTM! The fix correctly normalises path encoding for comparison.

The change ensures that both final.fullPath and fullPath are compared in their encoded forms, which resolves the issue where one side was percent-encoded (e.g., callback=%2Fother) whilst the other was decoded (e.g., callback=/other). The encodeURL utility consistently normalises both values before comparison, ensuring the redirect is triggered correctly.

test/basic.test.ts (1)

1125-1132: LGTM! The test correctly validates percent-encoding preservation.

The test verifies that navigateTo preserves percent-encoding in query parameters during server-side redirects. The assertion confirms that the forward slash in the callback parameter is correctly encoded as %2Fother in the redirect Location header, which addresses the issue described in #33273.

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 12, 2025

Open in StackBlitz

@nuxt/kit

npm i https://pkg.pr.new/@nuxt/kit@33465

nuxt

npm i https://pkg.pr.new/nuxt@33465

@nuxt/rspack-builder

npm i https://pkg.pr.new/@nuxt/rspack-builder@33465

@nuxt/schema

npm i https://pkg.pr.new/@nuxt/schema@33465

@nuxt/vite-builder

npm i https://pkg.pr.new/@nuxt/vite-builder@33465

@nuxt/webpack-builder

npm i https://pkg.pr.new/@nuxt/webpack-builder@33465

commit: 1d5a89a

@codspeed-hq
Copy link

codspeed-hq bot commented Oct 12, 2025

CodSpeed Performance Report

Merging #33465 will not alter performance

Comparing onmax:fix/navigate-to-percent-encoding (1d5a89a) with main (cf102e8)

Summary

✅ 10 untouched

@onmax onmax force-pushed the fix/navigate-to-percent-encoding branch 2 times, most recently from 8e5dd04 to d17547e Compare October 12, 2025 15:59
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: 0

🧹 Nitpick comments (1)
packages/nuxt/src/app/composables/router.ts (1)

217-217: Correct fix for percent-encoding preservation.

Applying encodeURL to both sides of the comparison ensures that paths are normalized to the same format before checking equality. This properly addresses the root cause where fullPath was decoded whilst final.fullPath was percent-encoded, causing the redirect check to fail.

Optional optimisation: Consider caching the encoded target to avoid recomputing on each navigation:

+      const encodedFullPath = encodeURL(fullPath)
-      router.afterEach(final => encodeURL(final.fullPath) === encodeURL(fullPath) ? redirect(false) : undefined)
+      router.afterEach(final => encodeURL(final.fullPath) === encodedFullPath ? redirect(false) : undefined)

This avoids repeatedly calling encodeURL(fullPath) since fullPath is captured in the closure and remains constant across navigation checks. However, the performance impact is negligible given this only runs server-side during middleware processing.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3852c90 and d17547e.

📒 Files selected for processing (5)
  • packages/nuxt/src/app/composables/router.ts (1 hunks)
  • test/basic.test.ts (1 hunks)
  • test/bundle.test.ts (1 hunks)
  • test/fixtures/basic/app/pages/navigate-to-encoded-query.vue (1 hunks)
  • test/fixtures/basic/app/pages/redirect-target.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/fixtures/basic/app/pages/navigate-to-encoded-query.vue
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • test/basic.test.ts
  • test/bundle.test.ts
  • packages/nuxt/src/app/composables/router.ts
**/*.{test,spec}.{ts,tsx,js,jsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Write unit tests for core functionality using vitest

Files:

  • test/basic.test.ts
  • test/bundle.test.ts
**/*.vue

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use <script setup lang="ts"> and the composition API when creating Vue components

Files:

  • test/fixtures/basic/app/pages/redirect-target.vue
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: test-fixtures (windows-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (windows-latest, built, rspack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, rspack, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, built, rspack, async, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-off, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, default, manifest-on, json, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, js, lts/-1)
  • GitHub Check: test-fixtures (ubuntu-latest, dev, vite, async, manifest-on, json, lts/-1)
  • GitHub Check: test-size
  • GitHub Check: typecheck (windows-latest, bundler)
  • GitHub Check: release-pkg-pr-new
  • GitHub Check: typecheck (ubuntu-latest, bundler)
  • GitHub Check: test-benchmark
  • GitHub Check: code
🔇 Additional comments (3)
test/fixtures/basic/app/pages/redirect-target.vue (1)

1-7: LGTM!

The component correctly uses the composition API with <script setup lang="ts"> as specified in the coding guidelines. The implementation appropriately serves as a redirect target for testing percent-encoding preservation.

test/bundle.test.ts (1)

120-120: Bundle size reduction is expected.

The 1k reduction in server bundle size aligns with the router changes that now use encodeURL for path comparison, resulting in slightly more efficient code generation.

test/basic.test.ts (1)

1125-1132: Excellent regression test coverage.

This test case directly verifies the fix for issue #33273 by ensuring that percent-encoded query parameters (specifically the forward slash as %2Fother) are preserved in redirect Location headers. The test expectations are clear and will prevent future regressions.

@onmax onmax force-pushed the fix/navigate-to-percent-encoding branch from 898ad95 to 1d5a89a Compare October 12, 2025 16:02
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: 0

🧹 Nitpick comments (1)
test/fixtures/basic/app/pages/navigate-to-encoded-query.vue (1)

5-9: Clarify comment to mention percent-encoding.

The comment on line 6 could be more precise to help readers understand the test's purpose. Consider mentioning that the forward slash in /other will be percent-encoded as %2Fother in the query parameter.

Apply this diff to improve the comment:

-// This should trigger a redirect to /redirect-target?callback=/other
+// This should trigger a redirect to /redirect-target?callback=%2Fother (encoded)
 // The query parameter contains a path that should be preserved

Additionally, verify that the corresponding test in test/basic.test.ts properly validates the Location header contains the percent-encoded query parameter (%2Fother).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d17547e and 1d5a89a.

📒 Files selected for processing (4)
  • packages/nuxt/src/app/composables/router.ts (1 hunks)
  • test/basic.test.ts (1 hunks)
  • test/fixtures/basic/app/pages/navigate-to-encoded-query.vue (1 hunks)
  • test/fixtures/basic/app/pages/redirect-target.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • test/fixtures/basic/app/pages/redirect-target.vue
  • test/basic.test.ts
  • packages/nuxt/src/app/composables/router.ts
🧰 Additional context used
📓 Path-based instructions (1)
**/*.vue

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use <script setup lang="ts"> and the composition API when creating Vue components

Files:

  • test/fixtures/basic/app/pages/navigate-to-encoded-query.vue
🔇 Additional comments (1)
test/fixtures/basic/app/pages/navigate-to-encoded-query.vue (1)

1-3: LGTM! Template appropriately signals redirect.

The placeholder text clearly indicates this page should not render, which is correct since the redirect should occur immediately in the script setup.

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: 0

🧹 Nitpick comments (2)
packages/nuxt/src/app/composables/router.ts (2)

217-217: Core fix correctly normalises both paths before comparison.

By encoding both final.fullPath and fullPath before comparing, this change ensures the equality check succeeds even when Vue Router returns them in different encoding states, which was the root cause of the redirect failure.

Optional micro-optimisation: since fullPath is constant in this scope, you could cache its encoded value to avoid encoding it on every afterEach invocation:

+      const encodedFullPath = encodeURL(fullPath)
-      router.afterEach(final => encodeURL(final.fullPath) === encodeURL(fullPath) ? redirect(false) : undefined)
+      router.afterEach(final => encodeURL(final.fullPath) === encodedFullPath ? redirect(false) : undefined)

However, since afterEach is only registered once per navigation and URL encoding is fast, the performance benefit would be negligible.


303-338: Consider using ufo's dedicated query encoding functions.

The decode-then-encode pattern for query parameters (lines 307-322) correctly normalises encoding, but manually parsing queries can miss edge cases.

Based on learnings, ufo v1.6.1 provides dedicated functions for query handling that may be more robust:

export function encodeURL (location: string, isExternalHost = false) {
  const url = parseURL(location)
  const pathname = encodePath(url.pathname)
  
  // Use ufo's parseQuery and withQuery for more robust query handling
  const query = url.search ? parseQuery(url.search) : {}
  const search = Object.keys(query).length > 0 
    ? '?' + Object.entries(query)
        .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(String(v))}`)
        .join('&')
    : ''
  
  const hash = url.hash ? encodeHash(url.hash) : ''
  
  if (!isExternalHost) {
    return pathname + search + hash
  }
  
  const protocol = url.protocol ? url.protocol + '//' : ''
  const host = url.host || ''
  const result = protocol + host + pathname + search + hash
  
  if (location.startsWith('//')) {
    return result.replace(/^https?:/, '')
  }
  return result
}

This approach leverages ufo's parseQuery (which now uses null-prototype objects for safety) and separates parsing concerns from encoding logic. However, the current implementation is explicit and working correctly, so this refactor is optional.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1d5a89a and 170ae28.

📒 Files selected for processing (2)
  • packages/nuxt/src/app/composables/router.ts (3 hunks)
  • test/fixtures/basic/app/pages/navigate-to-encoded-query.vue (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.vue

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use <script setup lang="ts"> and the composition API when creating Vue components

Files:

  • test/fixtures/basic/app/pages/navigate-to-encoded-query.vue
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/app/composables/router.ts
🧬 Code graph analysis (1)
packages/nuxt/src/app/composables/router.ts (1)
packages/nuxt/src/app/components/test-component-wrapper.ts (1)
  • url (8-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: code
🔇 Additional comments (2)
test/fixtures/basic/app/pages/navigate-to-encoded-query.vue (1)

1-13: LGTM! Well-structured test fixture.

The test fixture correctly demonstrates the percent-encoding preservation issue. The middleware redirect to /redirect-target?callback=/other will verify that the forward slash in the query parameter value is properly encoded as %2Fother in the final redirect location.

packages/nuxt/src/app/composables/router.ts (1)

5-5: LGTM! Appropriate imports for the encoding fix.

The addition of encodeHash and encodePath from ufo is necessary for the reworked encodeURL implementation and aligns with the library's purpose.

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: 2

🧹 Nitpick comments (2)
packages/nuxt/src/app/composables/router.ts (2)

217-217: LGTM with optional optimization suggestion.

The normalization approach correctly addresses the percent-encoding mismatch issue. Both paths are now encoded consistently before comparison, ensuring redirects work regardless of encoding differences.

For a minor performance improvement, consider caching the encoded target to avoid reparsing on every afterEach invocation:

+      const encodedFullPath = encodeURL(fullPath)
-      router.afterEach(final => encodeURL(final.fullPath) === encodeURL(fullPath) ? redirect(false) : undefined)
+      router.afterEach(final => encodeURL(final.fullPath) === encodedFullPath ? redirect(false) : undefined)

331-334: Add comment explaining protocol-relative URL handling.

The special case for URLs starting with // removes the protocol to preserve the protocol-relative format. This works correctly but is non-obvious and would benefit from a brief comment.

   if (location.startsWith('//')) {
+    // Protocol-relative URLs: preserve the '//' prefix by removing any protocol parseURL may have added
     return result.replace(/^https?:/, '')
   }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 170ae28 and 3e3f4c6.

📒 Files selected for processing (2)
  • packages/nuxt/src/app/composables/router.ts (3 hunks)
  • test/fixtures/basic/app/pages/navigate-to-encoded-query.vue (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/fixtures/basic/app/pages/navigate-to-encoded-query.vue
🧰 Additional context used
📓 Path-based instructions (1)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Follow standard TypeScript conventions and best practices

Files:

  • packages/nuxt/src/app/composables/router.ts
🧬 Code graph analysis (1)
packages/nuxt/src/app/composables/router.ts (1)
packages/nuxt/src/app/components/test-component-wrapper.ts (1)
  • url (8-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: codeql (javascript-typescript)
  • GitHub Check: build
  • GitHub Check: code

import type { NavigationFailure, NavigationGuard, RouteLocationNormalized, RouteLocationRaw, Router, useRoute as _useRoute, useRouter as _useRouter } from 'vue-router'
import { sanitizeStatusCode } from 'h3'
import { hasProtocol, isScriptProtocol, joinURL, parseQuery, parseURL, withQuery } from 'ufo'
import { encodeHash, encodePath, hasProtocol, isScriptProtocol, joinURL, parseQuery, parseURL, withQuery } from 'ufo'
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Consider importing stringifyQuery for safer query encoding.

The new imports are correctly used, but stringifyQuery from the ufo library would simplify the query encoding logic in encodeURL (lines 310-319) and handle edge cases like array values, empty strings, and malformed input more robustly. Based on learnings, ufo provides stringifyQuery specifically for this purpose.

Add stringifyQuery to the imports:

-import { encodeHash, encodePath, hasProtocol, isScriptProtocol, joinURL, parseQuery, parseURL, withQuery } from 'ufo'
+import { encodeHash, encodePath, hasProtocol, isScriptProtocol, joinURL, parseQuery, parseURL, stringifyQuery, withQuery } from 'ufo'
📝 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
import { encodeHash, encodePath, hasProtocol, isScriptProtocol, joinURL, parseQuery, parseURL, withQuery } from 'ufo'
import { encodeHash, encodePath, hasProtocol, isScriptProtocol, joinURL, parseQuery, parseURL, stringifyQuery, withQuery } from 'ufo'
🤖 Prompt for AI Agents
In packages/nuxt/src/app/composables/router.ts around line 5 and impacting
encodeURL at lines 310-319, the code manually builds/encodes query strings
instead of using ufo's stringifyQuery which better handles arrays, empty strings
and malformed input; update the import line to include stringifyQuery from 'ufo'
and replace the manual query encoding in encodeURL with a call to
stringifyQuery(query) (ensuring any pre-processing required by the function is
applied) so query serialization is delegated to the library and edge cases are
handled robustly.

Comment on lines 303 to 312
export function encodeURL (location: string, isExternalHost = false) {
const url = new URL(location, 'http://localhost')
const url = parseURL(location)
const pathname = encodePath(url.pathname)

// Encode query parameters by parsing and re-stringifying with proper encoding
// This normalizes encoding (e.g., preserves %2F for forward slashes in query values)
const query = parseQuery(url.search)
const search = Object.keys(query).length > 0
? '?' + Object.entries(query)
.map(([key, value]) => {
// Normalize by decoding and re-encoding to ensure consistent encoding
const encodedKey = encodeURIComponent(decodeURIComponent(String(key)))
const encodedValue = value ? encodeURIComponent(decodeURIComponent(String(value))) : ''
return encodedValue ? `${encodedKey}=${encodedValue}` : encodedKey
})
.join('&')
: ''

const hash = url.hash ? encodeHash(url.hash) : ''

if (!isExternalHost) {
return url.pathname + url.search + url.hash
return pathname + search + hash
}

const protocol = url.protocol ? url.protocol + '//' : ''
const host = url.host || ''
const result = protocol + host + pathname + search + hash

if (location.startsWith('//')) {
return url.toString().replace(url.protocol, '')
return result.replace(/^https?:/, '')
}
return url.toString()
return result
}
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

Simplify query encoding using stringifyQuery and fix array value handling.

The manual query encoding has several issues:

  1. Lines 314-315: The decode-then-encode pattern is problematic:

    • parseQuery already returns decoded values, so decodeURIComponent is redundant.
    • If values are double-encoded, this will triple-encode them.
    • decodeURIComponent can throw URIError on malformed input (no error handling).
  2. Array values: If a query parameter has multiple values (e.g., ?tag=a&tag=b), parseQuery returns { tag: ['a', 'b'] }. String(['a', 'b']) produces 'a,b', which is incorrect.

  3. Empty strings: Line 316 treats empty string values as falsy, generating ?key instead of ?key=.

Replace the manual query encoding with stringifyQuery:

-  // Encode query parameters by parsing and re-stringifying with proper encoding
-  // This normalizes encoding (e.g., preserves %2F for forward slashes in query values)
   const query = parseQuery(url.search)
-  const search = Object.keys(query).length > 0
-    ? '?' + Object.entries(query)
-      .map(([key, value]) => {
-        // Normalize by decoding and re-encoding to ensure consistent encoding
-        const encodedKey = encodeURIComponent(decodeURIComponent(String(key)))
-        const encodedValue = value ? encodeURIComponent(decodeURIComponent(String(value))) : ''
-        return encodedValue ? `${encodedKey}=${encodedValue}` : encodedKey
-      })
-      .join('&')
-    : ''
+  const search = Object.keys(query).length > 0 ? '?' + stringifyQuery(query) : ''

This leverages the ufo library's robust query encoding, which properly handles arrays, empty values, and special characters without the decode-then-encode issues.

📝 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
export function encodeURL (location: string, isExternalHost = false) {
const url = new URL(location, 'http://localhost')
const url = parseURL(location)
const pathname = encodePath(url.pathname)
// Encode query parameters by parsing and re-stringifying with proper encoding
// This normalizes encoding (e.g., preserves %2F for forward slashes in query values)
const query = parseQuery(url.search)
const search = Object.keys(query).length > 0
? '?' + Object.entries(query)
.map(([key, value]) => {
// Normalize by decoding and re-encoding to ensure consistent encoding
const encodedKey = encodeURIComponent(decodeURIComponent(String(key)))
const encodedValue = value ? encodeURIComponent(decodeURIComponent(String(value))) : ''
return encodedValue ? `${encodedKey}=${encodedValue}` : encodedKey
})
.join('&')
: ''
const hash = url.hash ? encodeHash(url.hash) : ''
if (!isExternalHost) {
return url.pathname + url.search + url.hash
return pathname + search + hash
}
const protocol = url.protocol ? url.protocol + '//' : ''
const host = url.host || ''
const result = protocol + host + pathname + search + hash
if (location.startsWith('//')) {
return url.toString().replace(url.protocol, '')
return result.replace(/^https?:/, '')
}
return url.toString()
return result
}
export function encodeURL (location: string, isExternalHost = false) {
const url = parseURL(location)
const pathname = encodePath(url.pathname)
const query = parseQuery(url.search)
const search = Object.keys(query).length > 0 ? '?' + stringifyQuery(query) : ''
const hash = url.hash ? encodeHash(url.hash) : ''
if (!isExternalHost) {
return pathname + search + hash
}
const protocol = url.protocol ? url.protocol + '//' : ''
const host = url.host || ''
const result = protocol + host + pathname + search + hash
if (location.startsWith('//')) {
return result.replace(/^https?:/, '')
}
return result
}
🤖 Prompt for AI Agents
In packages/nuxt/src/app/composables/router.ts around lines 303 to 335, the
manual query encoding is wrong (redundant decode/encode, throws on malformed
input, mishandles array values and empty strings); replace the custom
Object.entries mapping with the ufo.stringifyQuery utility (import if missing),
pass the parsed query object (from parseQuery(url.search)) to stringifyQuery and
prepend '?' when it returns a non-empty string so arrays and empty values are
encoded correctly, and remove the decodeURIComponent/encodeURIComponent logic
while preserving the existing pathname/hash and isExternalHost handling.

@onmax onmax force-pushed the fix/navigate-to-percent-encoding branch from 44d018f to 1d5a89a Compare October 12, 2025 17:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

navigateTo does not preserve percent-encoding in its query param

1 participant