Skip to content

Commit ed69b77

Browse files
goulinkhteemingc
andauthored
fix: remove unnecessary path validation which breaks fetch with custom path base (#15291)
closes #11078 When having a custom base path: ```js { kit: { paths: { base: '/ui' } } } ``` And making a request to a different server hosted on the same domain, `+layout.server.ts`: ```ts export const load: LayoutServerLoad = async ({ fetch }) => { const workingResponse = await fetch('/ui/api'); // works if api is defined in sveltekit const failingResponse = await fetch('/profile'); // returns 404, instead of making request and providing cookies } ``` Making a request to the same origin but to a different server not with the configured `paths.base`, currently doesn't work but this should be allowed and possible as they share the same domain name. --- ### Please don't delete this checklist! Before submitting the PR, please make sure you do the following: - [x] It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs - [x] This message body should clearly illustrate what problems it solves. - [x] Ideally, include a test that fails without this PR but passes with it. ### Tests - [x] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check` ### Changesets - [x] If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running `pnpm changeset` and following the prompts. Changesets that add features should be `minor` and those that fix bugs should be `patch`. Please prefix changeset messages with `feat:`, `fix:`, or `chore:`. ### Edits - [x] Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed. --------- Co-authored-by: Tee Ming <chewteeming01@gmail.com>
1 parent 2f3a007 commit ed69b77

File tree

10 files changed

+109
-3
lines changed

10 files changed

+109
-3
lines changed

.changeset/honest-cobras-jog.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sveltejs/kit': patch
3+
---
4+
5+
fix: `fetch` not working when URL is same host but different than `paths.base`

packages/kit/src/runtime/server/fetch.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,12 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
5555
request.headers.delete('origin');
5656
}
5757

58-
if (url.origin !== event.url.origin) {
58+
const decoded = decodeURIComponent(url.pathname);
59+
60+
if (
61+
url.origin !== event.url.origin ||
62+
(paths.base && decoded !== paths.base && !decoded.startsWith(`${paths.base}/`))
63+
) {
5964
// Allow cookie passthrough for "credentials: same-origin" and "credentials: include"
6065
// if SvelteKit is serving my.domain.com:
6166
// - domain.com WILL NOT receive cookies
@@ -77,7 +82,6 @@ export function create_fetch({ event, options, manifest, state, get_cookie_heade
7782
// handle fetch requests for static assets. e.g. prebaked data, etc.
7883
// we need to support everything the browser's fetch supports
7984
const prefix = paths.assets || paths.base;
80-
const decoded = decodeURIComponent(url.pathname);
8185
const filename = (
8286
decoded.startsWith(prefix) ? decoded.slice(prefix.length) : decoded
8387
).slice(1);

packages/kit/test/apps/dev-only/src/routes/request-abort/+page.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
fetch('/request-abort', { headers: { accept: 'application/json' } }).then(
1212
async (r) => (result = await r.json())
1313
);
14-
}, 100);
14+
}, 50);
1515
}
1616
1717
onMount(test_abort);
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function GET() {
2+
return new Response('root');
3+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export async function load({ fetch }) {
2+
const response = await fetch('/not-base-path/');
3+
return {
4+
fetchUrl: response.url,
5+
fetchResponse: await response.text()
6+
};
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<script>
2+
/** @type {import('./$types').PageProps} */
3+
const { data } = $props();
4+
</script>
5+
6+
<p data-testid="fetch-url">{data.fetchUrl}</p>
7+
<p data-testid="fetch-response">{data.fetchResponse}</p>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export async function load({ fetch }) {
2+
// fetch to root with trailing slash
3+
const response1 = await fetch('/', { redirect: 'manual' });
4+
// fetch to root without trailing slash
5+
const response2 = await fetch('', { redirect: 'manual' });
6+
return {
7+
fetches: [
8+
{
9+
url: response1.url,
10+
response: await response1.text(),
11+
redirect: response1.headers.get('location')
12+
},
13+
{
14+
url: response2.url,
15+
response: await response2.text(),
16+
redirect: response2.headers.get('location')
17+
}
18+
]
19+
};
20+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<script>
2+
/** @type {import('./$types').PageProps} */
3+
const { data } = $props();
4+
</script>
5+
6+
<h2>Fetch URLs</h2>
7+
8+
<dl>
9+
{#each data.fetches as item, index}
10+
<dt>fetch{index + 1}-url</dt>
11+
<dd data-testid={`fetch${index + 1}-url`}>{item.url}</dd>
12+
{/each}
13+
</dl>
14+
15+
<h2>Fetch Responses</h2>
16+
<dl>
17+
{#each data.fetches as item, index}
18+
<dt>fetch{index + 1}-response</dt>
19+
<dd data-testid="fetch{index + 1}-response">{item.response}</dd>
20+
{/each}
21+
</dl>
22+
23+
<h2>Fetch Redirects</h2>
24+
<dl>
25+
{#each data.fetches as item, index}
26+
<dt>fetch{index + 1}-redirect</dt>
27+
<dd data-testid="fetch{index + 1}-redirect">{item.redirect}</dd>
28+
{/each}
29+
</dl>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function GET() {
2+
return new Response('relative');
3+
}

packages/kit/test/apps/options/test/paths-assets.test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,34 @@ test.describe('base path', () => {
7777
expect(page.url()).toBe(`${baseURL}/path-base/resolve-route/resolved/`);
7878
expect(await page.textContent('h2')).toBe('resolved');
7979
});
80+
81+
test('server load fetch without base path does not invoke the server', async ({
82+
page,
83+
baseURL
84+
}) => {
85+
await page.goto('/path-base/fetch/link-outside-base/');
86+
await expect(page.locator('[data-testid="fetch-url"]')).toHaveText(`${baseURL}/not-base-path/`);
87+
await expect(page.locator('[data-testid="fetch-response"]')).toContainText(
88+
'did you mean to visit'
89+
);
90+
});
91+
92+
test('server load fetch to root does not invoke the server', async ({ page, baseURL }) => {
93+
await page.goto('/path-base/fetch/link-root/');
94+
// fetch to root with trailing slash
95+
await expect(page.locator('[data-testid="fetch1-url"]')).toHaveText(`${baseURL}/`);
96+
if (process.env.DEV) {
97+
await expect(page.locator('[data-testid="fetch1-redirect"]')).toHaveText('/path-base');
98+
} else {
99+
await expect(page.locator('[data-testid="fetch1-response"]')).toContainText(
100+
'did you mean to visit'
101+
);
102+
}
103+
104+
// fetch to root without trailing slash should be relative
105+
await expect(page.locator('[data-testid="fetch2-url"]')).toBeEmpty();
106+
await expect(page.locator('[data-testid="fetch2-response"]')).toHaveText('relative');
107+
});
80108
});
81109

82110
test.describe('assets path', () => {

0 commit comments

Comments
 (0)