-
Notifications
You must be signed in to change notification settings - Fork 0
fix: correctly serve ISR pages with trailing slash set to 'always' #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: qa/agent-sveltejs-kit/pr-05-16903/base
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| '@sveltejs/adapter-vercel': patch | ||
| --- | ||
|
|
||
| fix: pass the requested pathname, including any trailing slash, to ISR functions |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,6 +40,15 @@ jobs: | |
| vercel-token: ${{ secrets.VERCEL_TOKEN }} | ||
| vercel-org-id: ${{ secrets.VERCEL_ORG_ID }} | ||
|
|
||
| # avoid stale cache results when testing ISR routes | ||
| - name: Purge Vercel CDN Cache | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shipwright · HIGH The CDN cache purge runs unconditionally before the platform test and uses --yes. Impact: The CDN cache purge runs unconditionally before the platform test and uses --yes. If VERCEL_PROJECT_ID_BASIC is unset or wrong, the purge may target the wrong project or fail in a way that leaves stale cache entries, causing flaky ISR tests that are hard to diagnose. Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright. |
||
| shell: bash | ||
| env: | ||
| VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} | ||
| VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | ||
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID_BASIC }} | ||
| run: vercel cache purge --type cdn --token="${{ secrets.VERCEL_TOKEN }}" --yes | ||
|
|
||
| - uses: ./.github/actions/platform-test | ||
| with: | ||
| test-app-dir: packages/adapter-vercel/test/apps/basic | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,8 +10,6 @@ await server.init({ | |
| read: createReadableStream | ||
| }); | ||
|
|
||
| const DATA_SUFFIX = '/__data.json'; | ||
|
|
||
| export default { | ||
| /** | ||
| * @param {Request} request | ||
|
|
@@ -21,13 +19,10 @@ export default { | |
| // If this is an ISR request, the requested pathname is encoded | ||
| // as a search parameter, so we need to extract it | ||
| const url = new URL(request.url); | ||
| let pathname = url.searchParams.get('__pathname'); | ||
| const pathname = url.searchParams.get('__pathname'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shipwright · CRITICAL The ISR pathname is now taken directly from the route regex capture group and assigned to url.pathname without normalization. Impact: The ISR pathname is now taken directly from the route regex capture group and assigned to url.pathname without normalization. Previously the code collapsed repeated slashes and reconstructed the data suffix. With optional/rest route patterns, the captured value can contain empty or repeated slash segments, producing malformed pathnames such as /foo//bar or losing the __data.json suffix behavior, which can cause fals… Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright. |
||
|
|
||
| if (pathname) { | ||
| // Optional routes' pathname replacements look like `/foo/$1/bar` which means we could end up with an url like /foo//bar | ||
| pathname = pathname.replace(/\/+/g, '/'); | ||
|
|
||
| url.pathname = pathname + (url.pathname.endsWith(DATA_SUFFIX) ? DATA_SUFFIX : ''); | ||
| url.pathname = pathname; | ||
| url.searchParams.delete('__pathname'); | ||
|
|
||
| request = new Request(url, request); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,7 +4,7 @@ import process from 'node:process'; | |
| import { fileURLToPath } from 'node:url'; | ||
| import { VERSION } from '@sveltejs/kit'; | ||
| import { nodeFileTrace } from '@vercel/nft'; | ||
| import { get_pathname, parse_isr_expiration, pattern_to_src, resolve_runtime } from './utils.js'; | ||
| import { parse_isr_expiration, pattern_to_src, resolve_runtime } from './utils.js'; | ||
|
|
||
| const INTERNAL = '![-]'; // this name is guaranteed not to conflict with user routes | ||
|
|
||
|
|
@@ -230,6 +230,12 @@ const plugin = function (defaults = {}) { | |
| }); | ||
| } | ||
|
|
||
| // Vercel's filesystem phase serves a function at its own path, with or without a | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shipwright · HIGH The new static_isr_routes array is typed as any[] and the routing logic branches on route.segments.some((segment) => segment.dynamic) with no explanation of why static and dynamic Impact: The new static_isr_routes array is typed as any[] and the routing logic branches on route.segments.some((segment) => segment.dynamic) with no explanation of why static and dynamic ISR routes must be ordered differently. A future maintainer cannot tell from the code why static routes must precede filesystem while dynamic routes must follow it. Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright. |
||
| // trailing slash, before the routes below are consulted. Static ISR routes live at | ||
| // their own path, so they must be routed before it to arrive with `__pathname` | ||
| /** @type {any[]} */ | ||
| const static_isr_routes = []; | ||
|
|
||
| for (const route of builder.routes) { | ||
| if (is_prerendered(route)) continue; | ||
|
|
||
|
|
@@ -254,7 +260,6 @@ const plugin = function (defaults = {}) { | |
| fs.symlinkSync(`../${relative}`, `${base}/__data.json.func`); | ||
| } | ||
|
|
||
| const pathname = get_pathname(route); | ||
| const json = JSON.stringify( | ||
| { ...isr, expiration: parse_isr_expiration(isr.expiration, route.id) }, | ||
| null, | ||
|
|
@@ -266,17 +271,23 @@ const plugin = function (defaults = {}) { | |
| write(`${base}/__data.json.prerender-config.json`, json); | ||
| } | ||
|
|
||
| const q = `?__pathname=/${pathname}`; | ||
| const routes = route.segments.some((segment) => segment.dynamic) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shipwright · HIGH The route destination is built by interpolating src-derived pathname into a query string without URL-encoding. Impact: The route destination is built by interpolating src-derived pathname into a query string without URL-encoding. If a route pattern contains characters that are special in query strings, the generated __pathname parameter can be malformed or allow query-parameter injection into the ISR function request. Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright. |
||
| ? static_config.routes | ||
| : static_isr_routes; | ||
|
|
||
| static_config.routes.push({ | ||
| src: src + '$', | ||
| dest: `/${isr_name}${q}` | ||
| // capture the requested pathname (minus the `^` anchor) as `__pathname`, | ||
| // since the function otherwise only sees its own path | ||
| const pathname = src.slice(1); | ||
|
|
||
| routes.push({ | ||
| src: `^(${pathname})$`, | ||
| dest: `/${isr_name}?__pathname=$1` | ||
| }); | ||
|
|
||
| if (has_page) { | ||
| static_config.routes.push({ | ||
| src: src + '/__data.json$', | ||
| dest: `/${isr_name}/__data.json${q}` | ||
| routes.push({ | ||
| src: `^(${pathname}/__data.json)$`, | ||
| dest: `/${isr_name}/__data.json?__pathname=$1` | ||
| }); | ||
| } | ||
| } else { | ||
|
|
@@ -318,6 +329,9 @@ const plugin = function (defaults = {}) { | |
| } | ||
| } | ||
|
|
||
| const filesystem = static_config.routes.findIndex((route) => route.handle === 'filesystem'); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shipwright · CRITICAL The new static ISR route insertion assumes static_config.routes always contains a route with handle === 'filesystem'. Impact: The new static ISR route insertion assumes static_config.routes always contains a route with handle === 'filesystem'. If that route is absent, findIndex returns -1 and splice(-1, 0, ...static_isr_routes) inserts before the last route instead of before the filesystem phase, silently breaking routing order for static ISR routes. Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright. |
||
| static_config.routes.splice(filesystem, 0, ...static_isr_routes); | ||
|
|
||
| if (builder.config.router.resolution === 'server') { | ||
| // Create a separate serverless function just for server-side route resolution. | ||
| // By omitting all routes we're ensuring it's small (the routes will still be available | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,7 +21,7 @@ test('__data.json prerender config is not generated for server-only route', () = | |
| assert.ok(!fs.existsSync(`${functions}/__data.json.prerender-config.json`)); | ||
| }); | ||
|
|
||
| /** @type {{ routes: Array<{ src?: string }> }} */ | ||
| /** @type {{ routes: Array<{ src?: string, dest?: string, handle?: string }> }} */ | ||
| const config = JSON.parse(fs.readFileSync(`${output}/config.json`, 'utf8')); | ||
| const route_sources = config.routes.flatMap((route) => | ||
| typeof route.src === 'string' ? [route.src] : [] | ||
|
|
@@ -44,3 +44,20 @@ test('__data.json function exists for ISR page route', () => { | |
| test('__data.json function exists in Vercel routing configuration', () => { | ||
| assert.ok(isr_page_route_sources.some((src) => src.includes('__data.json'))); | ||
| }); | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shipwright · HIGH The test only asserts the generated config for one static ISR route and one dynamic ISR route. Impact: The test only asserts the generated config for one static ISR route and one dynamic ISR route. It does not cover optional parameters, rest parameters, or routes with multiple dynamic segments, which are exactly the cases where the removed get_pathname and regex normalization logic previously prevented malformed __pathname values. Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright. |
||
| const filesystem = config.routes.findIndex((route) => route.handle === 'filesystem'); | ||
|
|
||
| test('ISR routes capture the requested pathname', () => { | ||
| const route = config.routes.find((route) => route.src === '^(/isr-trailing-slash/?)$'); | ||
| assert.equal(route?.dest, '/isr-trailing-slash?__pathname=$1'); | ||
| }); | ||
|
|
||
| test('static ISR routes are matched before the filesystem', () => { | ||
| const index = config.routes.findIndex((route) => route.src === '^(/isr-trailing-slash/?)$'); | ||
| assert.ok(index < filesystem); | ||
| }); | ||
|
|
||
| test('dynamic ISR routes are matched after the filesystem', () => { | ||
| const index = config.routes.findIndex((route) => route.src === '^(/isr/([^/]+?)/?)$'); | ||
| assert.ok(index > filesystem); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| export const trailingSlash = 'always'; | ||
|
|
||
| export const config = { | ||
| isr: { | ||
| expiration: 60 | ||
| } | ||
| }; | ||
|
|
||
| export function load() { | ||
| return { | ||
| rendered_at: Date.now() | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| <script lang="ts"> | ||
| let { data } = $props(); | ||
| </script> | ||
|
|
||
| <h1>ISR Trailing Slash Page</h1> | ||
| <p id="rendered-at">{data.rendered_at}</p> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shipwright · CRITICAL
The workflow change exposes VERCEL_TOKEN and VERCEL_ORG_ID as environment variables and also interpolates VERCEL_TOKEN directly into the command line.
Impact: The workflow change exposes VERCEL_TOKEN and VERCEL_ORG_ID as environment variables and also interpolates VERCEL_TOKEN directly into the command line. The token is already available as a secret in the step, so the explicit env block and inline --token interpolation increase the risk of accidental logging or command-line disclosure. Use the existing vercel-action secret handling or pass the token via stdin/env withou…
Suggested fix: Review the cited evidence, fix the risk if confirmed, and rerun Shipwright.