|
| 1 | +# Fix for RSC Double Slashes Issue #14583 |
| 2 | + |
| 3 | +## Problem Description |
| 4 | + |
| 5 | +When using React Router with RSC (React Server Components), URLs containing double slashes (e.g., `https://domain//en//test2/test`) would cause `ERR_NAME_NOT_RESOLVED` errors when the router tried to load manifest files. This happened because the `getManifestUrl` function in `packages/react-router/lib/rsc/browser.tsx` was not normalizing double slashes in paths, leading to malformed URLs like `https://en//test/test.manifest`. |
| 6 | + |
| 7 | +## Root Cause |
| 8 | + |
| 9 | +The issue was in the `getManifestUrl` function which constructs URLs for manifest files without normalizing double slashes in: |
| 10 | +1. Single path manifest URLs |
| 11 | +2. Multiple paths in manifest URL query parameters |
| 12 | +3. Basename paths |
| 13 | + |
| 14 | +## Solution |
| 15 | + |
| 16 | +The fix normalizes double slashes using the existing `joinPaths` utility function from `../router/utils.ts`: |
| 17 | + |
| 18 | +### Changes Made |
| 19 | + |
| 20 | +1. **Import `joinPaths` utility**: Added `joinPaths` to the imports from `../router/utils` |
| 21 | +2. **Normalize single path URLs**: Use `joinPaths([paths[0]])` to normalize the path before creating the manifest URL |
| 22 | +3. **Normalize multiple paths**: Use `paths.map(path => joinPaths([path]))` to normalize all paths before joining them in the query parameter |
| 23 | +4. **Normalize basename**: Use `joinPaths([basename])` to normalize the basename |
| 24 | + |
| 25 | +### Code Changes |
| 26 | + |
| 27 | +**File**: `packages/react-router/lib/rsc/browser.tsx` |
| 28 | + |
| 29 | +```typescript |
| 30 | +// Before |
| 31 | +import { ErrorResponseImpl, createContext } from "../router/utils"; |
| 32 | + |
| 33 | +// After |
| 34 | +import { ErrorResponseImpl, createContext, joinPaths } from "../router/utils"; |
| 35 | +``` |
| 36 | + |
| 37 | +```typescript |
| 38 | +// Before |
| 39 | +function getManifestUrl(paths: string[]): URL | null { |
| 40 | + if (paths.length === 0) { |
| 41 | + return null; |
| 42 | + } |
| 43 | + |
| 44 | + if (paths.length === 1) { |
| 45 | + return new URL(`${paths[0]}.manifest`, window.location.origin); |
| 46 | + } |
| 47 | + |
| 48 | + const globalVar = window as WindowWithRouterGlobals; |
| 49 | + let basename = (globalVar.__reactRouterDataRouter.basename ?? "").replace( |
| 50 | + /^\/|\/$/g, |
| 51 | + "", |
| 52 | + ); |
| 53 | + let url = new URL(`${basename}/.manifest`, window.location.origin); |
| 54 | + url.searchParams.set("paths", paths.sort().join(",")); |
| 55 | + |
| 56 | + return url; |
| 57 | +} |
| 58 | + |
| 59 | +// After |
| 60 | +function getManifestUrl(paths: string[]): URL | null { |
| 61 | + if (paths.length === 0) { |
| 62 | + return null; |
| 63 | + } |
| 64 | + |
| 65 | + if (paths.length === 1) { |
| 66 | + // Normalize double slashes in the single path |
| 67 | + const normalizedPath = joinPaths([paths[0]]); |
| 68 | + return new URL(`${normalizedPath}.manifest`, window.location.origin); |
| 69 | + } |
| 70 | + |
| 71 | + const globalVar = window as WindowWithRouterGlobals; |
| 72 | + let basename = (globalVar.__reactRouterDataRouter.basename ?? "").replace( |
| 73 | + /^\/|\/$/g, |
| 74 | + "", |
| 75 | + ); |
| 76 | + // Normalize double slashes in basename |
| 77 | + basename = joinPaths([basename]); |
| 78 | + let url = new URL(`${basename}/.manifest`, window.location.origin); |
| 79 | + // Normalize double slashes in all paths before joining |
| 80 | + const normalizedPaths = paths.map(path => joinPaths([path])); |
| 81 | + url.searchParams.set("paths", normalizedPaths.sort().join(",")); |
| 82 | + |
| 83 | + return url; |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +## Testing |
| 88 | + |
| 89 | +### Test Cases Verified |
| 90 | + |
| 91 | +1. **Single path with double slashes**: `//en//test2/test` → `/en/test2/test.manifest` |
| 92 | +2. **Multiple paths with double slashes**: `[//en//test1, //fr//test2]` → query param `paths=/en/test1,/fr/test2` |
| 93 | +3. **Basename with double slashes**: `/app//base/` → `/app/base/.manifest` |
| 94 | +4. **Normal paths**: Continue to work as expected |
| 95 | + |
| 96 | +### Test Files Created |
| 97 | + |
| 98 | +- `packages/react-router/__tests__/rsc-double-slashes-test.ts`: Unit tests for URL normalization |
| 99 | +- `integration/rsc-double-slashes-test.ts`: Integration tests for RSC functionality |
| 100 | +- `test-double-slashes-fix.js`: Verification script for the fix |
| 101 | +- `test-rsc-double-slashes-integration.js`: Integration verification script |
| 102 | + |
| 103 | +## Impact |
| 104 | + |
| 105 | +This fix resolves the `ERR_NAME_NOT_RESOLVED` errors that occur when: |
| 106 | +- Users navigate to URLs with double slashes in RSC applications |
| 107 | +- The router attempts to fetch manifest files for route discovery |
| 108 | +- Basename configurations contain double slashes |
| 109 | + |
| 110 | +## Backward Compatibility |
| 111 | + |
| 112 | +This change is fully backward compatible as it only normalizes URLs that would have been malformed anyway. Normal URLs without double slashes continue to work exactly as before. |
| 113 | + |
| 114 | +## Related Issue |
| 115 | + |
| 116 | +Fixes #14583: RSC / Routing Issue on Double Slashes |
| 117 | + |
| 118 | +## Files Modified |
| 119 | + |
| 120 | +- `packages/react-router/lib/rsc/browser.tsx`: Main fix implementation |
| 121 | +- `packages/react-router/__tests__/rsc-double-slashes-test.ts`: Unit tests |
| 122 | +- `integration/rsc-double-slashes-test.ts`: Integration tests |
0 commit comments