Repository: sveltejs/kit
Pull request #7453 (2022-10-31 18:17:25)

Read mode/credentials from `init` object, not `request`
This is an alternative to #7192. Rather than checking for a `cf` property, it reads `mode` and `credentials` from the second argument passed to `fetch`, unless `fetch` was called with a `Request` object (which is a somewhat unusual case; code that does `fetch(request)` would have to be refactored to `fetch(url, init)` on Cloudflare). 

I'm slightly ambivalent about which approach is better. This gets rid of the Cloudflare-specific `cf` stuff, and in theory is more resilient to other runtimes doing the same thing, but it would force the aforementioned refactoring in a very small minority of cases.

### 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.
- [ ] 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



Patch:
diff --git a/.changeset/tidy-tables-think.md b/.changeset/tidy-tables-think.md
new file mode 100644
index 000000000..a79fb94e8
--- /dev/null
+++ b/.changeset/tidy-tables-think.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[fix] fetch erroring on Cloudflare
diff --git a/packages/adapter-static/README.md b/packages/adapter-static/README.md
index 540239ae8..8da5c9dbf 100644
--- a/packages/adapter-static/README.md
+++ b/packages/adapter-static/README.md
@@ -138,6 +138,21 @@ You can also add turn off prerendering only to parts of your app, if you want ot
 
 During development, SvelteKit will still attempt to server-side render your routes. This means accessing things that are only available in the browser (such as the `window` object) will result in errors, even though this would be valid in the output app. To align the behavior of SvelteKit's dev mode with your SPA, you can [add `export const ssr = false` to your root `+layout`](https://kit.svelte.dev/docs/page-options#ssr). You can also add this option only to parts of your app, if you want other parts to be prerendered.
 
+### Apache
+
+To run an SPA on [Apache](https://httpd.apache.org/), you should add a `static/.htaccess` file to route requests to the fallback page:
+
+```
+<IfModule mod_rewrite.c>
+  RewriteEngine On
+  RewriteBase /
+  RewriteRule ^200\.html$ - [L]
+  RewriteCond %{REQUEST_FILENAME} !-f
+  RewriteCond %{REQUEST_FILENAME} !-d
+  RewriteRule . /200.html [L]
+</IfModule>
+```
+
 ## GitHub Pages
 
 When building for GitHub Pages, make sure to update [`paths.base`](https://kit.svelte.dev/docs/configuration#paths) to match your repo name, since the site will be served from <https://your-username.github.io/your-repo-name> rather than from the root.
diff --git a/packages/kit/src/runtime/server/fetch.js b/packages/kit/src/runtime/server/fetch.js
index 1632db5b8..74b36ff8e 100644
--- a/packages/kit/src/runtime/server/fetch.js
+++ b/packages/kit/src/runtime/server/fetch.js
@@ -12,13 +12,19 @@ import { respond } from './index.js';
  */
 export function create_fetch({ event, options, state, get_cookie_header }) {
 	return async (info, init) => {
-		const request = normalize_fetch_input(info, init, event.url);
+		const original_request = normalize_fetch_input(info, init, event.url);
 
 		const request_body = init?.body;
 
+		// some runtimes (e.g. Cloudflare) error if you access `request.mode`,
+		// annoyingly, so we need to read the value from the `init` object instead
+		let mode = (info instanceof Request ? info.mode : init?.mode) ?? 'cors';
+		let credentials =
+			(info instanceof Request ? info.credentials : init?.credentials) ?? 'same-origin';
+
 		return await options.hooks.handleFetch({
 			event,
-			request,
+			request: original_request,
 			fetch: async (info, init) => {
 				const request = normalize_fetch_input(info, init, event.url);
 
@@ -28,10 +34,16 @@ export function create_fetch({ event, options, state, get_cookie_header }) {
 					request.headers.set('origin', event.url.origin);
 				}
 
+				if (info !== original_request) {
+					mode = (info instanceof Request ? info.mode : init?.mode) ?? 'cors';
+					credentials =
+						(info instanceof Request ? info.credentials : init?.credentials) ?? 'same-origin';
+				}
+
 				// Remove Origin, according to https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Origin#description
 				if (
 					(request.method === 'GET' || request.method === 'HEAD') &&
-					((request.mode === 'no-cors' && url.origin !== event.url.origin) ||
+					((mode === 'no-cors' && url.origin !== event.url.origin) ||
 						url.origin === event.url.origin)
 				) {
 					request.headers.delete('origin');
@@ -46,17 +58,14 @@ export function create_fetch({ event, options, state, get_cookie_header }) {
 					// - sub.my.domain.com WILL receive cookies
 					// ports do not affect the resolution
 					// leading dot prevents mydomain.com matching domain.com
-					if (
-						`.${url.hostname}`.endsWith(`.${event.url.hostname}`) &&
-						request.credentials !== 'omit'
-					) {
+					if (`.${url.hostname}`.endsWith(`.${event.url.hostname}`) && credentials !== 'omit') {
 						const cookie = get_cookie_header(url, request.headers.get('cookie'));
 						if (cookie) request.headers.set('cookie', cookie);
 					}
 
 					let response = await fetch(request);
 
-					if (request.mode === 'no-cors') {
+					if (mode === 'no-cors') {
 						response = new Response('', {
 							status: response.status,
 							statusText: response.statusText,
@@ -109,7 +118,7 @@ export function create_fetch({ event, options, state, get_cookie_header }) {
 					return await fetch(request);
 				}
 
-				if (request.credentials !== 'omit') {
+				if (credentials !== 'omit') {
 					const cookie = get_cookie_header(url, request.headers.get('cookie'));
 					if (cookie) {
 						request.headers.set('cookie', cookie);
diff --git a/packages/kit/src/runtime/server/page/render.js b/packages/kit/src/runtime/server/page/render.js
index b24a9c14f..b6ca8aa2d 100644
--- a/packages/kit/src/runtime/server/page/render.js
+++ b/packages/kit/src/runtime/server/page/render.js
@@ -246,7 +246,7 @@ export async function render_response({
 				env: ${s(options.public_env)},
 				hydrate: ${page_config.ssr ? `{
 					status: ${status},
-					error: ${s(error)},
+					error: ${devalue.uneval(error)},
 					node_ids: [${branch.map(({ node }) => node.index).join(', ')}],
 					params: ${devalue.uneval(event.params)},
 					routeId: ${s(event.routeId)},


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/xss/[path]/+page.svelte b/packages/kit/test/apps/basics/src/routes/xss/[path]/+page.svelte
index d4f913913..e8dcc4cff 100644
--- a/packages/kit/test/apps/basics/src/routes/xss/[path]/+page.svelte
+++ b/packages/kit/test/apps/basics/src/routes/xss/[path]/+page.svelte
@@ -2,4 +2,4 @@
 	import { page } from '$app/stores';
 </script>
 
-<h1 id="one">{$page.params.slug}</h1>
+<h1 id="one">{$page.params.path}</h1>
diff --git a/packages/kit/test/apps/basics/test/test.js b/packages/kit/test/apps/basics/test/test.js
index b352c5b40..70a377f9c 100644
--- a/packages/kit/test/apps/basics/test/test.js
+++ b/packages/kit/test/apps/basics/test/test.js
@@ -1729,16 +1729,23 @@ test.describe('XSS', () => {
 		}
 	});
 
-	const uri_xss_payload = encodeURIComponent('</script><script>window.pwned=1</script>');
+	const uri_xss_payload = '</script><script>window.pwned=1</script>';
+	const uri_xss_payload_encoded = encodeURIComponent(uri_xss_payload);
+
 	test('no xss via dynamic route path', async ({ page }) => {
-		await page.goto(`/xss/${uri_xss_payload}`);
+		await page.goto(`/xss/${uri_xss_payload_encoded}`);
+
+		expect(await page.textContent('h1')).toBe(uri_xss_payload);
 
 		// @ts-expect-error - check global injected variable
 		expect(await page.evaluate(() => window.pwned)).toBeUndefined();
 	});
 
 	test('no xss via query param', async ({ page }) => {
-		await page.goto(`/xss/query?key=${uri_xss_payload}`);
+		await page.goto(`/xss/query?key=${uri_xss_payload_encoded}`);
+
+		expect(await page.textContent('#one')).toBe(JSON.stringify({ key: [uri_xss_payload] }));
+		expect(await page.textContent('#two')).toBe(JSON.stringify({ key: [uri_xss_payload] }));
 
 		// @ts-expect-error - check global injected variable
 		expect(await page.evaluate(() => window.pwned)).toBeUndefined();
diff --git a/packages/kit/test/utils.js b/packages/kit/test/utils.js
index ed1c58d0b..a60ec8e56 100644
--- a/packages/kit/test/utils.js
+++ b/packages/kit/test/utils.js
@@ -141,7 +141,7 @@ export const config = {
 		command: process.env.DEV ? 'pnpm dev' : 'pnpm build && pnpm preview',
 		port: process.env.DEV ? 5173 : 4173
 	},
-	retries: process.env.CI ? 5 : 0,
+	retries: process.env.CI ? 2 : 0,
 	projects: [
 		{
 			name: `${test_browser}-${process.env.DEV ? 'dev' : 'build'}`,