Repository: sveltejs/kit
Pull request #5025 (2022-05-22 07:25:16)

[fix] resolve fetch requests from the target page
When using `load`'s `fetch`, it's natural to assume URLs will be resolved relatively to the navigation's target page. Instead, they take the current page into account. This means that, in practice, we must always use absolute or root-relative URLs.

This PR resolves URLs relatively to the target page before issuing the native `fetch`. It's a simple fix, with just a couple of wrinkles we must pay attention to:

- The `Request` object is mostly read-only, so we need to consume its body and manually copy properties into the new request;
- Our ssr-included payloads wouldn't match the full URLs, so we skip normalization during hydration.

### Please don't delete this checklist! Before submitting the PR, please make sure you do the following:
- [ ] 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. All changesets should be `patch` until SvelteKit 1.0


changeset-bot[bot]: ###  🦋  Changeset detected

Latest commit: f5bb10a4640e55e377092b0c9f84519a79b05f6e

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name          | Type  |
| ------------- | ----- |
| @sveltejs/kit | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/mrkishi/kit/new/relative-fetch?filename=.changeset/clean-ways-matter.md&value=---%0A%22%40sveltejs%2Fkit%22%3A%20patch%0A%22test-basics%22%3A%20patch%0A---%0A%0A%5Bfix%5D%20resolve%20fetch%20requests%20from%20the%20target%20page%0A)



Patch:
diff --git a/.changeset/forty-lions-give.md b/.changeset/forty-lions-give.md
new file mode 100644
index 000000000..1c0228231
--- /dev/null
+++ b/.changeset/forty-lions-give.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[breaking] resolve relative urls from the target page when using load's fetch
diff --git a/package.json b/package.json
index de108360f..86af6277f 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
 		"turbo": "^1.2.6",
 		"typescript": "~4.6.2"
 	},
-	"packageManager": "pnpm@7.1.2",
+	"packageManager": "pnpm@7.1.3",
 	"engines": {
 		"pnpm": "^7.0.0"
 	},
diff --git a/packages/kit/src/runtime/client/client.js b/packages/kit/src/runtime/client/client.js
index fc28a2310..3962f7d92 100644
--- a/packages/kit/src/runtime/client/client.js
+++ b/packages/kit/src/runtime/client/client.js
@@ -578,11 +578,44 @@ export function create_client({ target, session, base, trailing_slash }) {
 					node.uses.stuff = true;
 					return { ...stuff };
 				},
-				fetch(resource, info) {
-					const requested = typeof resource === 'string' ? resource : resource.url;
-					add_dependency(requested);
+				async fetch(resource, init) {
+					let requested;
+
+					if (typeof resource === 'string') {
+						requested = resource;
+					} else {
+						requested = resource.url;
+
+						// we're not allowed to modify the received `Request` object, so in order
+						// to fixup relative urls we create a new equivalent `init` object instead
+						init = {
+							// the request body must be consumed in memory until browsers
+							// implement streaming request bodies and/or the body getter
+							body:
+								resource.method === 'GET' || resource.method === 'HEAD'
+									? undefined
+									: await resource.blob(),
+							cache: resource.cache,
+							credentials: resource.credentials,
+							headers: resource.headers,
+							integrity: resource.integrity,
+							keepalive: resource.keepalive,
+							method: resource.method,
+							mode: resource.mode,
+							redirect: resource.redirect,
+							referrer: resource.referrer,
+							referrerPolicy: resource.referrerPolicy,
+							signal: resource.signal,
+							...init
+						};
+					}
+
+					// we must fixup relative urls so they are resolved from the target page
+					const normalized = new URL(requested, url).href;
+					add_dependency(normalized);
 
-					return started ? fetch(resource, info) : initial_fetch(resource, info);
+					// prerendered pages may be served from any origin, so `initial_fetch` urls shouldn't be normalized
+					return started ? fetch(normalized, init) : initial_fetch(requested, init);
 				},
 				status: status ?? null,
 				error: error ?? null
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index b38836ec1..8753ec783 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -5735,7 +5735,6 @@ packages:
   /svelte/3.44.2:
     resolution: {integrity: sha512-jrZhZtmH3ZMweXg1Q15onb8QlWD+a5T5Oca4C1jYvSURp2oD35h4A5TV6t6MEa93K4LlX6BkafZPdQoFjw/ylA==}
     engines: {node: '>= 8'}
-    dev: true
 
   /svelte/3.48.0:
     resolution: {integrity: sha512-fN2YRm/bGumvjUpu6yI3BpvZnpIm9I6A7HR4oUNYd7ggYyIwSA/BX7DJ+UXXffLp6XNcUijyLvttbPVCYa/3xQ==}
diff --git a/turbo.json b/turbo.json
index a26407fa7..2afc03905 100644
--- a/turbo.json
+++ b/turbo.json
@@ -7,12 +7,12 @@
 			"outputs": ["dist/**", "docs/**", "assets/**"]
 		},
 		"kit.svelte.dev#build": {
-			"dependsOn": ["^build", "$VERCEL"],
+			"dependsOn": ["^build", "$VERCEL", "$ENABLE_VC_BUILD"],
 			"inputs": ["src/**", "../../packages/kit/docs/**", "../../documentation/**"],
 			"outputs": [".vercel_build_output/**"]
 		},
 		"build": {
-			"dependsOn": ["^build", "$VERCEL"],
+			"dependsOn": ["^build", "$VERCEL", "$ENABLE_VC_BUILD"],
 			"inputs": ["src/**", "scripts/**", "shared/**", "templates/**"],
 			"outputs": ["files/**", "dist/**", ".svelte-kit/**", ".vercel_build_output/**"]
 		},


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-relative.json.js b/packages/kit/test/apps/basics/src/routes/load/fetch-relative.json.js
new file mode 100644
index 000000000..6cabf0982
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-relative.json.js
@@ -0,0 +1,16 @@
+export function get() {
+	return {
+		body: {
+			answer: 42
+		}
+	};
+}
+
+/** @type {import('@sveltejs/kit').RequestHandler} */
+export async function post({ request }) {
+	return {
+		body: {
+			question: await request.text()
+		}
+	};
+}
diff --git a/packages/kit/test/apps/basics/src/routes/load/fetch-relative.svelte b/packages/kit/test/apps/basics/src/routes/load/fetch-relative.svelte
new file mode 100644
index 000000000..441822668
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/load/fetch-relative.svelte
@@ -0,0 +1,19 @@
+<script context="module">
+	/** @type {import('@sveltejs/kit').Load} */
+	export async function load({ fetch }) {
+		const get = await fetch('fetch-relative.json');
+		const post = await fetch('fetch-relative.json', { method: 'post', body: '?' });
+
+		return {
+			props: { ...(await get.json()), ...(await post.json()) }
+		};
+	}
+</script>
+
+<script>
+	export let answer;
+	export let question;
+</script>
+
+<h1>the answer is {answer}</h1>
+<h2>the question was {question}</h2>
diff --git a/packages/kit/test/apps/basics/src/routes/load/index.svelte b/packages/kit/test/apps/basics/src/routes/load/index.svelte
index 5cffb7e2a..181c11684 100644
--- a/packages/kit/test/apps/basics/src/routes/load/index.svelte
+++ b/packages/kit/test/apps/basics/src/routes/load/index.svelte
@@ -21,6 +21,7 @@
 <h1>bar == {foo}?</h1>
 
 <a href="/load/fetch-request">fetch request</a>
+<a href="/load/fetch-relative">fetch relative</a>
 <a href="/load/fetch-credentialed">fetch credentialed</a>
 <a href="/load/fetch-headers">fetch headers</a>
 <a href="/load/large-response">large response</a>
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index faae28cd8..1ca8f1457 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1311,6 +1311,13 @@ test.describe.parallel('Load', () => {
 		expect(await page.textContent('h1')).toBe('the answer is 42');
 	});
 
+	test('fetch resolves urls relatively to the target page', async ({ page, clicknav }) => {
+		await page.goto('/load');
+		await clicknav('[href="/load/fetch-relative"]');
+		expect(await page.textContent('h1')).toBe('the answer is 42');
+		expect(await page.textContent('h2')).toBe('the question was ?');
+	});
+
 	test('handles large responses', async ({ page }) => {
 		await page.goto('/load');