Repository: sveltejs/kit (js)

Issue:
`fetch` of URL with space fails during prerendering with `npm run build`
**Describe the bug**
When fetching an URL that contains spaces e.g. `/my static file.md` (escaped `/my%20static%20file.md`), the `fetch` tries to access a double escaped url `/my%20static%20file.md` (escaped `/my%2520static%2520file.md`) and generates a 404 html page in the file system under `/build/my%20static%20file.md/index.html`. On the browser side, the fetch works.

**Logs**
The build fails with:

```
> 404 /articles/recipe/Crushing%20bird%20nests%20at%20Nardah.md (fetched from /recipe/iiGzJN9IX0ILE81ECwNuIpJ6LUOsWbigIvMuTIkOxs9ISrFZQIMBb8pJybuBzdsZQQ)
```

**To Reproduce**

Make a route with a load function that fetches an URL with spaces in it. Use adapter static. The file will be fetched correctly in the browser but will fail during generation with `npm run build`.

I can make an example repo later if needed.

**Expected behavior**
Build passing and no `/build/my%20static%20file.md/index.html` file generated.

**Stacktraces**
If you have a stack trace to include, we recommend putting inside a `<details>` block for the sake of the thread's readability:

<details>
  <summary>Stack trace</summary>

> 404 /articles/recipe/Crushing%20bird%20nests%20at%20Nardah.md (fetched from /recipe/iiGzJN9IX0ILE81ECwNuIpJ6LUOsWbigIvMuTIkOxs9ISrFZQIMBb8pJybuBzdsZQQ)
Error: 404 /articles/recipe/Crushing%20bird%20nests%20at%20Nardah.md (fetched from /recipe/iiGzJN9IX0ILE81ECwNuIpJ6LUOsWbigIvMuTIkOxs9ISrFZQIMBb8pJybuBzdsZQQ)
    at error (file:///C:/Users/a/src/greedy-goblin/node_modules/@sveltejs/kit/dist/chunks/index5.js:93:11)
    at file:///C:/Users/a/src/greedy-goblin/node_modules/@sveltejs/kit/dist/chunks/index5.js:181:6
    at Map.forEach (<anonymous>)
    at visit (file:///C:/Users/a/src/greedy-goblin/node_modules/@sveltejs/kit/dist/chunks/index5.js:163:17)
    at async visit (file:///C:/Users/a/src/greedy-goblin/node_modules/@sveltejs/kit/dist/chunks/index5.js:220:6)
    at async visit (file:///C:/Users/a/src/greedy-goblin/node_modules/@sveltejs/kit/dist/chunks/index5.js:220:6)
    at async prerender (file:///C:/Users/a/src/greedy-goblin/node_modules/@sveltejs/kit/dist/chunks/index5.js:229:5)
    at async Object.prerender (file:///C:/Users/a/src/greedy-goblin/node_modules/@sveltejs/kit/dist/chunks/index5.js:296:5)
    at async adapt (file:///C:/Users/a/src/greedy-goblin/node_modules/@sveltejs/adapter-static/index.js:17:4)
    at async adapt (file:///C:/Users/a/src/greedy-goblin/node_modules/@sveltejs/kit/dist/chunks/index5.js:322:2)

</details>

**Information about your SvelteKit Installation:**

<details>
  <summary>Diagnostics</summary>

 System:
    OS: Windows 10 10.0.19042
    CPU: (4) x64 Intel(R) Core(TM) i5-4460  CPU @ 3.20GHz
    Memory: 4.36 GB / 15.90 GB
  Binaries:
    Node: 14.15.1 - C:\Program Files\nodejs\node.EXE
    npm: 7.13.0 - ~\src\greedy-goblin\node_modules\.bin\npm.CMD
  Browsers:
    Edge: Spartan (44.19041.964.0), Chromium (90.0.818.62)
    Internet Explorer: 11.0.19041.1
  npmPackages:
    @sveltejs/kit: next => 1.0.0-next.107
    svelte: ^3.29.0 => 3.38.2
    vite: ^2.1.0 => 2.3.2

using adapter-static
</details>

**Severity**

This can be circumvented with only using file names that don't need to be escaped. But, considering that this error stops the build, is quite subtle and can take some time to debug. Maybe it could be considered a medium-level priority.

**Additional context**
Discussion about the bug when it was discovered https://discord.com/channels/457912077277855764/819723698415599626/844895934257037392

Pull request description:
Decode URIs before checking filename against manifest
Fixes #1501 — `fetch('/file with spaces.json')` and `fetch('/file%20with%20spaces.json')` are treated the same way when `static/file with spaces.json` exists and we're prerendering

### 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 `pnpx changeset` and following the prompts. All changesets should be `patch` until SvelteKit 1.0


Patch:
diff --git a/.changeset/eight-readers-thank.md b/.changeset/eight-readers-thank.md
new file mode 100644
index 000000000..498b9f6a8
--- /dev/null
+++ b/.changeset/eight-readers-thank.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+fix handling an incoming request from HTTP/2
diff --git a/.changeset/pre.json b/.changeset/pre.json
index a775af371..279b56e65 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -134,6 +134,7 @@
     "eight-glasses-sparkle",
     "eight-hats-worry",
     "eight-keys-give",
+    "eight-readers-thank",
     "eighty-candles-grow",
     "eighty-carrots-heal",
     "eighty-waves-obey",
diff --git a/.changeset/proud-buses-hug.md b/.changeset/proud-buses-hug.md
new file mode 100644
index 000000000..98213f1c8
--- /dev/null
+++ b/.changeset/proud-buses-hug.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Decode fetched resources before checking against manifest when prerendering
diff --git a/.changeset/serious-horses-rule.md b/.changeset/serious-horses-rule.md
new file mode 100644
index 000000000..9932eb971
--- /dev/null
+++ b/.changeset/serious-horses-rule.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+Breaking: remove -H and (conflicting) -h shortcuts from CLI
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 0e0256107..52de0d8ec 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/kit
 
+## 1.0.0-next.247
+
+### Patch Changes
+
+- fix handling an incoming request from HTTP/2 ([#3572](https://github.com/sveltejs/kit/pull/3572))
+
 ## 1.0.0-next.246
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 4bdbad79d..59855a0d9 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.246",
+	"version": "1.0.0-next.247",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/cli.js b/packages/kit/src/cli.js
index cf5e532e3..44284332c 100755
--- a/packages/kit/src/cli.js
+++ b/packages/kit/src/cli.js
@@ -45,11 +45,14 @@ prog
 	.command('dev')
 	.describe('Start a development server')
 	.option('-p, --port', 'Port')
-	.option('-h, --host', 'Host (only use this on trusted networks)')
-	.option('-H, --https', 'Use self-signed HTTPS certificate')
 	.option('-o, --open', 'Open a browser tab')
-	.action(async ({ port, host, https, open }) => {
+	.option('--host', 'Host (only use this on trusted networks)')
+	.option('--https', 'Use self-signed HTTPS certificate')
+	.option('-H', 'no longer supported, use --https instead') // TODO remove for 1.0
+	.action(async ({ port, host, https, open, H }) => {
 		try {
+			if (H) throw new Error('-H is no longer supported — use --https instead');
+
 			process.env.NODE_ENV = process.env.NODE_ENV || 'development';
 			const config = await load_config();
 
@@ -118,11 +121,14 @@ prog
 	.command('preview')
 	.describe('Serve an already-built app')
 	.option('-p, --port', 'Port', 3000)
-	.option('-h, --host', 'Host (only use this on trusted networks)', 'localhost')
-	.option('-H, --https', 'Use self-signed HTTPS certificate', false)
 	.option('-o, --open', 'Open a browser tab', false)
-	.action(async ({ port, host, https, open }) => {
+	.option('--host', 'Host (only use this on trusted networks)', 'localhost')
+	.option('--https', 'Use self-signed HTTPS certificate', false)
+	.option('-H', 'no longer supported, use --https instead') // TODO remove for 1.0
+	.action(async ({ port, host, https, open, H }) => {
 		try {
+			if (H) throw new Error('-H is no longer supported — use --https instead');
+
 			await check_port(port);
 
 			process.env.NODE_ENV = process.env.NODE_ENV || 'production';
diff --git a/packages/kit/src/core/dev/index.js b/packages/kit/src/core/dev/index.js
index a1e9a948e..3bcd1a9b1 100644
--- a/packages/kit/src/core/dev/index.js
+++ b/packages/kit/src/core/dev/index.js
@@ -85,14 +85,6 @@ export async function dev({ cwd, port, host, https, config }) {
 		merged_config.server.https = https;
 	}
 
-	// by default, when enabling HTTPS in Vite, it also enables HTTP/2
-	// however, node-fetch's Request implementation does not like the HTTP/2 headers
-	// we set a no-op proxy config to force Vite to downgrade to TLS-only
-	// see https://vitejs.dev/config/#server-https
-	if (merged_config.server.https && !merged_config.server.proxy) {
-		merged_config.server.proxy = {};
-	}
-
 	if (port) {
 		merged_config.server.port = port;
 	}
diff --git a/packages/kit/src/core/dev/plugin.js b/packages/kit/src/core/dev/plugin.js
index 763475d54..97af10bda 100644
--- a/packages/kit/src/core/dev/plugin.js
+++ b/packages/kit/src/core/dev/plugin.js
@@ -25,7 +25,7 @@ export async function create_plugin(config, cwd) {
 
 	if (config.kit.amp) {
 		process.env.VITE_SVELTEKIT_AMP = 'true';
-		amp = (await import('./amp_hook')).handle;
+		amp = (await import('./amp_hook.js')).handle;
 	}
 
 	return {
diff --git a/packages/kit/src/node.js b/packages/kit/src/node.js
index 44d3ae388..5e70731c7 100644
--- a/packages/kit/src/node.js
+++ b/packages/kit/src/node.js
@@ -52,9 +52,19 @@ function get_raw_body(req) {
 
 /** @type {import('@sveltejs/kit/node').GetRequest} */
 export async function getRequest(base, req) {
+	let headers = /** @type {Record<string, string>} */ (req.headers);
+	if (req.httpVersionMajor === 2) {
+		// we need to strip out the HTTP/2 pseudo-headers because node-fetch's
+		// Request implementation doesn't like them
+		headers = Object.assign({}, headers);
+		delete headers[':method'];
+		delete headers[':path'];
+		delete headers[':authority'];
+		delete headers[':scheme'];
+	}
 	return new Request(base + req.url, {
 		method: req.method,
-		headers: /** @type {Record<string, string>} */ (req.headers),
+		headers,
 		body: await get_raw_body(req) // TODO stream rather than buffer
 	});
 }
diff --git a/packages/kit/src/runtime/server/page/load_node.js b/packages/kit/src/runtime/server/page/load_node.js
index 66a61a9ff..bb878ae14 100644
--- a/packages/kit/src/runtime/server/page/load_node.js
+++ b/packages/kit/src/runtime/server/page/load_node.js
@@ -105,7 +105,7 @@ export async function load_node({
 				// handle fetch requests for static assets. e.g. prebaked data, etc.
 				// we need to support everything the browser's fetch supports
 				const prefix = options.paths.assets || options.paths.base;
-				const filename = (
+				const filename = decodeURIComponent(
 					resolved.startsWith(prefix) ? resolved.slice(prefix.length) : resolved
 				).slice(1);
 				const filename_html = `${filename}/index.html`; // path may also match path/index.html


Test patch:
diff --git a/packages/kit/test/prerendering/basics/src/routes/load-file-with-spaces.svelte b/packages/kit/test/prerendering/basics/src/routes/load-file-with-spaces.svelte
new file mode 100644
index 000000000..b0a3d165f
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/src/routes/load-file-with-spaces.svelte
@@ -0,0 +1,25 @@
+<script context="module">
+	/** @type {import('@sveltejs/kit').Load} */
+	export async function load({ fetch }) {
+		const r1 = await fetch('/file%20with%20spaces.json');
+		const p1 = await r1.json();
+
+		const r2 = await fetch('/file with spaces.json');
+		const p2 = await r2.json();
+
+		if (p1.answer !== p2.answer) {
+			throw new Error('oops');
+		}
+
+		return {
+			props: p1
+		};
+	}
+</script>
+
+<script>
+	/** @type {number} */
+	export let answer;
+</script>
+
+<h1>answer: {answer}</h1>
diff --git a/packages/kit/test/prerendering/basics/static/file with spaces.json b/packages/kit/test/prerendering/basics/static/file with spaces.json
new file mode 100644
index 000000000..6e887fd6a
--- /dev/null
+++ b/packages/kit/test/prerendering/basics/static/file with spaces.json
@@ -0,0 +1,1 @@
+{ "answer": 42 }
diff --git a/packages/kit/test/prerendering/basics/test/test.js b/packages/kit/test/prerendering/basics/test/test.js
index 16972c334..2cef10ee3 100644
--- a/packages/kit/test/prerendering/basics/test/test.js
+++ b/packages/kit/test/prerendering/basics/test/test.js
@@ -58,4 +58,9 @@ test('renders page with unbuffered data from endpoint', () => {
 	assert.equal(json, JSON.stringify({ answer: 42 }));
 });
 
+test('loads a file with spaces in the filename', () => {
+	const content = read('load-file-with-spaces/index.html');
+	assert.ok(content.includes('<h1>answer: 42</h1>'), content);
+});
+
 test.run();