diff --git a/.changeset/honest-pandas-itch.md b/.changeset/honest-pandas-itch.md new file mode 100644 index 000000000000..e152884724e1 --- /dev/null +++ b/.changeset/honest-pandas-itch.md @@ -0,0 +1,6 @@ +--- +'@sveltejs/adapter-node': patch +'@sveltejs/kit': patch +--- + +fix: redirect trailing slash normalization relatively instead of against root diff --git a/packages/adapter-node/src/handler.js b/packages/adapter-node/src/handler.js index 37827e64042c..3bb6bc369fa5 100644 --- a/packages/adapter-node/src/handler.js +++ b/packages/adapter-node/src/handler.js @@ -5,7 +5,12 @@ import process from 'node:process'; import sirv from 'sirv'; import { fileURLToPath } from 'node:url'; import { parse as polka_url_parser } from '@polka/url'; -import { getRequest, setResponse, createReadableStream } from '@sveltejs/kit/node'; +import { + getRequest, + setResponse, + createReadableStream, + relative_pathname +} from '@sveltejs/kit/node'; import { Server } from 'SERVER'; import { manifest, prerendered, base } from 'MANIFEST'; import { env } from 'ENV'; @@ -81,8 +86,11 @@ function serve_prerendered() { } // remove or add trailing slash as appropriate - let location = pathname.at(-1) === '/' ? pathname.slice(0, -1) : pathname + '/'; - if (prerendered.has(location)) { + const inverted_trailing_slash = + pathname.at(-1) === '/' ? pathname.slice(0, -1) : pathname + '/'; + if (prerendered.has(inverted_trailing_slash)) { + // ensure preservation of (possibly invisible) path prefixes + let location = relative_pathname(pathname, inverted_trailing_slash); if (query) location += search; res.writeHead(308, { location }).end(); } else { diff --git a/packages/kit/package.json b/packages/kit/package.json index 8cfa35a03d5f..f4e44757d054 100644 --- a/packages/kit/package.json +++ b/packages/kit/package.json @@ -22,6 +22,7 @@ "cookie": "^0.6.0", "devalue": "^5.1.0", "esm-env": "^1.2.2", + "get-relative-path": "^1.0.2", "import-meta-resolve": "^4.1.0", "kleur": "^4.1.5", "magic-string": "^0.30.5", diff --git a/packages/kit/src/exports/node/index.js b/packages/kit/src/exports/node/index.js index a69b7ae6d906..30047973f343 100644 --- a/packages/kit/src/exports/node/index.js +++ b/packages/kit/src/exports/node/index.js @@ -222,3 +222,5 @@ export async function setResponse(res, response) { export function createReadableStream(file) { return /** @type {ReadableStream} */ (Readable.toWeb(createReadStream(file))); } + +export { relative_pathname } from '../../utils/url.js'; diff --git a/packages/kit/src/runtime/server/respond.js b/packages/kit/src/runtime/server/respond.js index 81b30e0756a5..cc70989002ff 100644 --- a/packages/kit/src/runtime/server/respond.js +++ b/packages/kit/src/runtime/server/respond.js @@ -11,7 +11,13 @@ import { method_not_allowed, redirect_response } from './utils.js'; -import { decode_pathname, decode_params, disable_search, normalize_path } from '../../utils/url.js'; +import { + decode_pathname, + decode_params, + disable_search, + normalize_path, + relative_pathname +} from '../../utils/url.js'; import { exec } from '../../utils/routing.js'; import { redirect_json_response, render_data } from './data/index.js'; import { add_cookies_to_headers, get_cookies } from './cookie.js'; @@ -321,9 +327,8 @@ export async function respond(request, options, manifest, state) { headers: { 'x-sveltekit-normalize': '1', location: - // ensure paths starting with '//' are not treated as protocol-relative - (normalized.startsWith('//') ? url.origin + normalized : normalized) + - (url.search === '?' ? '' : url.search) + // ensure preservation of (possibly invisible) path prefixes + relative_pathname(url.pathname, normalized) + (url.search === '?' ? '' : url.search) } }); } diff --git a/packages/kit/src/utils/url.js b/packages/kit/src/utils/url.js index 7ea2c9d100f1..6b3bf0d8b524 100644 --- a/packages/kit/src/utils/url.js +++ b/packages/kit/src/utils/url.js @@ -1,4 +1,5 @@ import { BROWSER, DEV } from 'esm-env'; +import getRelativePath from 'get-relative-path'; /** * Matches a URI scheme. See https://www.rfc-editor.org/rfc/rfc3986#section-3.1 @@ -77,6 +78,18 @@ export function decode_uri(uri) { } } +/** + * Calculates the relative path between two URL pathnames. + * Note that `relative` from `node:path` works with file system paths which are subtly diffent. + * For example: it ignores trailing slashes, which are significant in URLs. + * @param {string} from + * @param {string} to + */ +export function relative_pathname(from, to) { + // TODO inline + return getRelativePath(from, to); +} + /** * Returns everything up to the first `#` in a URL * @param {{href: string}} url_like diff --git a/packages/kit/types/index.d.ts b/packages/kit/types/index.d.ts index 23bb6d7287a4..a45c69c4faee 100644 --- a/packages/kit/types/index.d.ts +++ b/packages/kit/types/index.d.ts @@ -2145,6 +2145,12 @@ declare module '@sveltejs/kit/node' { * @since 2.4.0 */ export function createReadableStream(file: string): ReadableStream; + /** + * Calculates the relative path between two URL pathnames. + * Note that `relative` from `node:path` works with file system paths which are subtly diffent. + * For example: it ignores trailing slashes, which are significant in URLs. + * */ + export function relative_pathname(from: string, to: string): string; export {}; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1e8b3d6ebe9d..f068ca415348 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -368,6 +368,9 @@ importers: esm-env: specifier: ^1.2.2 version: 1.2.2 + get-relative-path: + specifier: ^1.0.2 + version: 1.0.2 import-meta-resolve: specifier: ^4.1.0 version: 4.1.0 @@ -2510,6 +2513,9 @@ packages: resolution: {integrity: sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==} engines: {node: '>=8'} + get-relative-path@1.0.2: + resolution: {integrity: sha512-dGkopYfmB4sXMTcZslq5SojEYakpdCSj/SVSHLhv7D6RBHzvDtd/3Q8lTEOAhVKxPPeAHu/YYkENbbz3PaH+8w==} + get-source@2.0.12: resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==} @@ -4888,6 +4894,8 @@ snapshots: get-port@5.1.1: {} + get-relative-path@1.0.2: {} + get-source@2.0.12: dependencies: data-uri-to-buffer: 2.0.2