Repository: sveltejs/kit
Pull request #6879 (2022-09-19 03:44:09)

fix: return 404 instead of 200 for stale assets with `adapter-cloudflare`
Hello, this is my first PR, so please bear with me.

After experiencing numerous problems when deploying new code to Cloudflare Pages with stale assets and cache settings (e.g. when  Argo or Early Hints services were enabled), I discovered that requesting a non-existent asset from the `_app` directory always returned a status code of 200 and the contents of the static `404.html` file if it existed, or nothing at all if it didn't. This caused a lot of issues for our users because they had to hard refresh our web page after a new deployment in order to invalidate the wrong cached content in their clients due to some bizarre Cloudflare Edge issues—this was needed even after purging the Cloudflare's cache.

Instead of constructing a new response object and returning it as if the asset was there, I updated the Cloudflare adapter to return early if the asset was not found. I also included a check before caching the server response to avoid caching if the response was not ok.

### 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.
- [ ] Ideally, include a test that fails without this PR but passes with it.

### Tests
- [ ] Run the tests with `pnpm test` and lint the project with `pnpm lint` and `pnpm check`

### Changesets
- [ ] 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]: ###  ⚠️  No Changeset found

Latest commit: 8c8ca877d306ffe845f94fc8008c4eeb399d4185

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/master/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/raduab/kit/new/master?filename=.changeset/silent-scissors-rest.md&value=---%0A%22%40sveltejs%2Fadapter-cloudflare%22%3A%20patch%0A---%0A%0Afix%3A%20return%20404%20instead%20of%20200%20for%20stale%20assets%20with%20%60adapter-cloudflare%60%0A)



Patch:
diff --git a/.changeset/healthy-rivers-share.md b/.changeset/healthy-rivers-share.md
new file mode 100644
index 000000000..8aac9d659
--- /dev/null
+++ b/.changeset/healthy-rivers-share.md
@@ -0,0 +1,5 @@
+---
+"@sveltejs/adapter-cloudflare": patch
+---
+
+[fix] return 404 instead of 200 for missing assets
diff --git a/.changeset/lazy-mice-remain.md b/.changeset/lazy-mice-remain.md
new file mode 100644
index 000000000..519d0d7fd
--- /dev/null
+++ b/.changeset/lazy-mice-remain.md
@@ -0,0 +1,5 @@
+---
+'@sveltejs/kit': patch
+---
+
+[feat] allow +server.js files next to +page files
diff --git a/.changeset/pre.json b/.changeset/pre.json
index 1dfbec7e7..55db167c0 100644
--- a/.changeset/pre.json
+++ b/.changeset/pre.json
@@ -210,6 +210,7 @@
     "cyan-knives-mix",
     "cyan-numbers-change",
     "cyan-parents-enjoy",
+    "cyan-pianos-bake",
     "cyan-radios-attend",
     "cyan-roses-act",
     "cyan-schools-kiss",
@@ -271,6 +272,7 @@
     "empty-worms-appear",
     "fair-pets-bake",
     "fair-rivers-repair",
+    "fair-tables-peel",
     "famous-birds-boil",
     "famous-birds-cheer",
     "famous-boxes-tap",
@@ -536,6 +538,7 @@
     "lazy-ants-watch",
     "lazy-berries-decide",
     "lazy-carpets-join",
+    "lazy-mice-remain",
     "lazy-mice-smile",
     "lazy-owls-run",
     "lazy-spies-wash",
diff --git a/documentation/docs/03-routing.md b/documentation/docs/03-routing.md
index ac5598dd7..3ca29c382 100644
--- a/documentation/docs/03-routing.md
+++ b/documentation/docs/03-routing.md
@@ -267,6 +267,57 @@ The first argument to `Response` can be a [`ReadableStream`](https://developer.m
 
 You can use the `error`, `redirect` and `json` methods from `@sveltejs/kit` for convenience (but you don't have to). Note that `throw error(..)` only returns a plain text error response.
 
+#### Receiving data
+
+By exporting `POST`/`PUT`/`PATCH`/`DELETE` handlers, `+server.js` files can be used to create a complete API:
+
+```svelte
+/// file: src/routes/add/+page.svelte
+<script>
+	let a = 0;
+	let b = 0;
+	let total = 0;
+
+	async function add() {
+		const response = await fetch('/api/add', {
+			method: 'POST',
+			body: JSON.stringify({ a, b }),
+			headers: {
+				'content-type': 'application/json'
+			}
+		});
+
+		total = await response.json();
+	}
+</script>
+
+<input type="number" bind:value={a}> +
+<input type="number" bind:value={b}> =
+{total}
+
+<button on:click={add}>Calculate</button>
+```
+
+```js
+/// file: src/routes/api/add/+server.js
+import { json } from '@sveltejs/kit';
+
+/** @type {import('./$types').RequestHandler} */
+export async function POST({ request }) {
+	const { a, b } = await request.json();
+	return json(a + b);
+}
+```
+
+> In general, [form actions](/docs/form-actions) are a better way to submit data from the browser to the server.
+
+#### Content negotiation
+
+`+server.js` files can be placed in the same directory as `+page` files, allowing the same route to be either a page or an API endpoint. To determine which, SvelteKit applies the following rules:
+
+- `PUT`/`PATCH`/`DELETE` requests are always handled by `+server.js` since they do not apply to pages
+- `GET`/`POST` requests are treated as page requests if the `accept` header prioritises `text/html` (in other words, it's a browser page request), else they are handled by `+server.js`
+
 ### $types
 
 Throughout the examples above, we've been importing types from a `$types.d.ts` file. This is a file SvelteKit creates for you in a hidden directory if you're using TypeScript (or JavaScript with JSDoc type annotations) to give you type safety when working with your root files.
diff --git a/documentation/docs/06-form-actions.md b/documentation/docs/06-form-actions.md
index fcf1300a8..d5fe40ad9 100644
--- a/documentation/docs/06-form-actions.md
+++ b/documentation/docs/06-form-actions.md
@@ -351,3 +351,7 @@ We can also implement progressive enhancement ourselves, without `use:enhance`,
 	<!-- content -->
 </form>
 ```
+
+### Alternatives
+
+Form actions are the preferred way to send data to the server, since they can be progressively enhanced, but you can also use [`+server.js`](/docs/routing#server) files to expose (for example) a JSON API.
\ No newline at end of file
diff --git a/packages/adapter-cloudflare/src/worker.js b/packages/adapter-cloudflare/src/worker.js
index a31267aef..d3223615f 100644
--- a/packages/adapter-cloudflare/src/worker.js
+++ b/packages/adapter-cloudflare/src/worker.js
@@ -20,6 +20,7 @@ const worker = {
 		// static assets
 		if (pathname.startsWith(prefix)) {
 			res = await env.ASSETS.fetch(req);
+			if (!res.ok) return res;
 
 			const cache_control = pathname.startsWith(prefix + 'immutable/')
 				? 'public, immutable, max-age=31536000'
@@ -65,7 +66,7 @@ const worker = {
 
 		// Writes to Cache only if allowed & specified
 		pragma = res.headers.get('cache-control');
-		return pragma ? Cache.save(req, res, context) : res;
+		return pragma && res.ok ? Cache.save(req, res, context) : res;
 	}
 };
 
diff --git a/packages/kit/CHANGELOG.md b/packages/kit/CHANGELOG.md
index 7b8e26355..7b467086e 100644
--- a/packages/kit/CHANGELOG.md
+++ b/packages/kit/CHANGELOG.md
@@ -1,5 +1,17 @@
 # @sveltejs/kit
 
+## 1.0.0-next.487
+
+### Patch Changes
+
+- [feat] allow +server.js files next to +page files ([#6773](https://github.com/sveltejs/kit/pull/6773))
+
+## 1.0.0-next.486
+
+### Patch Changes
+
+- [fix] handle jsdoc without tags while generating proxy types ([#6884](https://github.com/sveltejs/kit/pull/6884))
+
 ## 1.0.0-next.485
 
 ### Patch Changes
diff --git a/packages/kit/package.json b/packages/kit/package.json
index 8fdaa7aa4..b25610318 100644
--- a/packages/kit/package.json
+++ b/packages/kit/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/kit",
-	"version": "1.0.0-next.485",
+	"version": "1.0.0-next.487",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",
diff --git a/packages/kit/src/core/adapt/builder.js b/packages/kit/src/core/adapt/builder.js
index b79fb68d7..f8dcc36d1 100644
--- a/packages/kit/src/core/adapt/builder.js
+++ b/packages/kit/src/core/adapt/builder.js
@@ -46,7 +46,6 @@ export function create_builder({ config, build_data, routes, prerendered, log })
 
 				return {
 					id: route.id,
-					type: route.page ? 'page' : 'endpoint', // TODO change this if support pages+endpoints
 					segments: route.id.split('/').map((segment) => ({
 						dynamic: segment.includes('['),
 						rest: segment.includes('[...'),
diff --git a/packages/kit/src/core/sync/create_manifest_data/index.js b/packages/kit/src/core/sync/create_manifest_data/index.js
index c386bf1ed..563c391b0 100644
--- a/packages/kit/src/core/sync/create_manifest_data/index.js
+++ b/packages/kit/src/core/sync/create_manifest_data/index.js
@@ -272,11 +272,6 @@ function create_routes_and_nodes(cwd, config, fallback) {
 		route_map.forEach((route) => {
 			if (!route.leaf) return;
 
-			if (route.leaf && route.endpoint) {
-				// TODO possibly relax this https://github.com/sveltejs/kit/issues/5896
-				throw new Error(`${route.endpoint.file} cannot share a directory with other route files`);
-			}
-
 			route.page = {
 				layouts: [],
 				errors: [],
diff --git a/packages/kit/src/runtime/server/endpoint.js b/packages/kit/src/runtime/server/endpoint.js
index 5e3aba084..f80ed4417 100644
--- a/packages/kit/src/runtime/server/endpoint.js
+++ b/packages/kit/src/runtime/server/endpoint.js
@@ -1,4 +1,5 @@
 import { json } from '../../exports/index.js';
+import { negotiate } from '../../utils/http.js';
 import { Redirect, ValidationError } from '../control.js';
 import { check_method_names, method_not_allowed } from './utils.js';
 
@@ -64,3 +65,19 @@ export async function render_endpoint(event, mod, state) {
 		throw error;
 	}
 }
+
+/**
+ * @param {import('types').RequestEvent} event
+ */
+export function is_endpoint_request(event) {
+	const { method } = event.request;
+
+	if (method === 'PUT' || method === 'PATCH' || method === 'DELETE') {
+		// These methods exist exclusively for endpoints
+		return true;
+	}
+
+	// GET/POST requests may be for endpoints or pages. We prefer endpoints if this isn't a text/html request
+	const accept = event.request.headers.get('accept') ?? '*/*';
+	return negotiate(accept, ['*', 'text/html']) !== 'text/html';
+}
diff --git a/packages/kit/src/runtime/server/index.js b/packages/kit/src/runtime/server/index.js
index cff90f3c0..9070608a0 100644
--- a/packages/kit/src/runtime/server/index.js
+++ b/packages/kit/src/runtime/server/index.js
@@ -1,4 +1,4 @@
-import { render_endpoint } from './endpoint.js';
+import { is_endpoint_request, render_endpoint } from './endpoint.js';
 import { render_page } from './page/index.js';
 import { render_response } from './page/render.js';
 import { respond_with_error } from './page/respond_with_error.js';
@@ -226,10 +226,10 @@ export async function respond(request, options, state) {
 
 				if (is_data_request) {
 					response = await render_data(event, route, options, state);
+				} else if (route.endpoint && (!route.page || is_endpoint_request(event))) {
+					response = await render_endpoint(event, await route.endpoint(), state);
 				} else if (route.page) {
 					response = await render_page(event, route, route.page, options, state, resolve_opts);
-				} else if (route.endpoint) {
-					response = await render_endpoint(event, await route.endpoint(), state);
 				} else {
 					// a route will always have a page or an endpoint, but TypeScript
 					// doesn't know that
diff --git a/packages/kit/types/private.d.ts b/packages/kit/types/private.d.ts
index 24b587ddc..18a2c1091 100644
--- a/packages/kit/types/private.d.ts
+++ b/packages/kit/types/private.d.ts
@@ -195,7 +195,6 @@ export interface RequestOptions {
 
 export interface RouteDefinition {
 	id: string;
-	type: 'page' | 'endpoint';
 	pattern: RegExp;
 	segments: RouteSegment[];
 	methods: HttpMethod[];
diff --git a/packages/package/CHANGELOG.md b/packages/package/CHANGELOG.md
index dc82b369c..0fbb8b088 100644
--- a/packages/package/CHANGELOG.md
+++ b/packages/package/CHANGELOG.md
@@ -1,5 +1,11 @@
 # @sveltejs/package
 
+## 1.0.0-next.4
+
+### Patch Changes
+
+- [fix] don't strip `type="application/.."` tags ([#6887](https://github.com/sveltejs/kit/pull/6887))
+
 ## 1.0.0-next.3
 
 ### Patch Changes
diff --git a/packages/package/package.json b/packages/package/package.json
index 357a0da4a..b5bf86863 100644
--- a/packages/package/package.json
+++ b/packages/package/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "@sveltejs/package",
-	"version": "1.0.0-next.3",
+	"version": "1.0.0-next.4",
 	"repository": {
 		"type": "git",
 		"url": "https://github.com/sveltejs/kit",


Test patch:
diff --git a/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte
new file mode 100644
index 000000000..b88ab6af7
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+page.svelte
@@ -0,0 +1,18 @@
+<script>
+	let result;
+
+	/** @param {string} method */
+	async function request(method) {
+		result = 'loading';
+		const response = await fetch('/routing/endpoint-next-to-page', {method});
+		result = await response.text();
+	}
+</script>
+
+<p>Hi</p>
+<button on:click={() => request('GET')}>GET</button>
+<button on:click={() => request('PUT')}>PUT</button>
+<button on:click={() => request('PATCH')}>PATCH</button>
+<button on:click={() => request('POST')}>POST</button>
+<button on:click={() => request('DELETE')}>DELETE</button>
+<pre>{result}</pre>
diff --git a/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js
new file mode 100644
index 000000000..007c90f0d
--- /dev/null
+++ b/packages/kit/test/apps/basics/src/routes/routing/endpoint-next-to-page/+server.js
@@ -0,0 +1,24 @@
+/** @type {import('./$types').RequestHandler} */
+export function GET() {
+	return new Response('GET');
+}
+
+/** @type {import('./$types').RequestHandler} */
+export function PUT() {
+	return new Response('PUT');
+}
+
+/** @type {import('./$types').RequestHandler} */
+export function PATCH() {
+	return new Response('PATCH');
+}
+
+/** @type {import('./$types').RequestHandler} */
+export function POST() {
+	return new Response('POST');
+}
+
+/** @type {import('./$types').RequestHandler} */
+export function DELETE() {
+	return new Response('DELETE');
+}
diff --git a/packages/kit/test/apps/basics/test/client.test.js b/packages/kit/test/apps/basics/test/client.test.js
index cf12fdc98..42dedf5b7 100644
--- a/packages/kit/test/apps/basics/test/client.test.js
+++ b/packages/kit/test/apps/basics/test/client.test.js
@@ -8,7 +8,7 @@ test.skip(({ javaScriptEnabled }) => !javaScriptEnabled);
 test.describe.configure({ mode: 'parallel' });
 
 test.describe('beforeNavigate', () => {
-	test('prevents navigation triggered by link click', async ({ clicknav, page, baseURL }) => {
+	test('prevents navigation triggered by link click', async ({ page, baseURL }) => {
 		await page.goto('/before-navigate/prevent-navigation');
 
 		await page.click('[href="/before-navigate/a"]');
@@ -807,3 +807,23 @@ test.describe('data-sveltekit attributes', () => {
 		expect(await page.evaluate(() => window.scrollY)).toBe(0);
 	});
 });
+
+test('+server.js next to +page.svelte works', async ({ page }) => {
+	await page.goto('/routing/endpoint-next-to-page');
+	expect(await page.textContent('p')).toBe('Hi');
+
+	await page.click('button:has-text("GET")');
+	await expect(page.locator('pre')).toHaveText('GET');
+
+	await page.click('button:has-text("PUT")');
+	await expect(page.locator('pre')).toHaveText('PUT');
+
+	await page.click('button:has-text("PATCH")');
+	await expect(page.locator('pre')).toHaveText('PATCH');
+
+	await page.click('button:has-text("POST")');
+	await expect(page.locator('pre')).toHaveText('POST');
+
+	await page.click('button:has-text("DELETE")');
+	await expect(page.locator('pre')).toHaveText('DELETE');
+});