From d3bd673392e63720e241d6a002a131a3564c169c Mon Sep 17 00:00:00 2001
From: Bjorn Lu <bjornlu.dev@gmail.com>
Date: Thu, 19 Sep 2024 20:43:01 +0800
Subject: [PATCH] Check route collision after getStaticPaths (#12028)

---
 .changeset/light-pianos-sip.md                     |  5 +++++
 packages/astro/src/core/render/params-and-props.ts | 11 +++++------
 .../astro/test/dynamic-endpoint-collision.test.js  |  5 +++++
 .../src/pages/api/safe/[...slug].ts                | 14 ++++++++++++++
 4 files changed, 29 insertions(+), 6 deletions(-)
 create mode 100644 .changeset/light-pianos-sip.md
 create mode 100644 packages/astro/test/fixtures/dynamic-endpoint-collision/src/pages/api/safe/[...slug].ts

diff --git a/.changeset/light-pianos-sip.md b/.changeset/light-pianos-sip.md
new file mode 100644
index 0000000000..c9f979c37c
--- /dev/null
+++ b/.changeset/light-pianos-sip.md
@@ -0,0 +1,5 @@
+---
+'astro': patch
+---
+
+Handles route collision detection only if it matches `getStaticPaths`
diff --git a/packages/astro/src/core/render/params-and-props.ts b/packages/astro/src/core/render/params-and-props.ts
index cf7d02d483..6f5ec19f63 100644
--- a/packages/astro/src/core/render/params-and-props.ts
+++ b/packages/astro/src/core/render/params-and-props.ts
@@ -33,12 +33,6 @@ export async function getProps(opts: GetParamsAndPropsOptions): Promise<Props> {
 		return {};
 	}
 
-	// This is a dynamic route, start getting the params
-	const params = getParams(route, pathname);
-	if (mod) {
-		validatePrerenderEndpointCollision(route, mod, params);
-	}
-
 	// During build, the route cache should already be populated.
 	// During development, the route cache is filled on-demand and may be empty.
 	const staticPaths = await callGetStaticPaths({
@@ -49,6 +43,7 @@ export async function getProps(opts: GetParamsAndPropsOptions): Promise<Props> {
 		ssr: serverLike,
 	});
 
+	const params = getParams(route, pathname);
 	const matchedStaticPath = findPathItemByKey(staticPaths, params, route, logger);
 	if (!matchedStaticPath && (serverLike ? route.prerender : true)) {
 		throw new AstroError({
@@ -58,6 +53,10 @@ export async function getProps(opts: GetParamsAndPropsOptions): Promise<Props> {
 		});
 	}
 
+	if (mod) {
+		validatePrerenderEndpointCollision(route, mod, params);
+	}
+
 	const props: Props = matchedStaticPath?.props ? { ...matchedStaticPath.props } : {};
 
 	return props;
diff --git a/packages/astro/test/dynamic-endpoint-collision.test.js b/packages/astro/test/dynamic-endpoint-collision.test.js
index b1aa42f9fd..91bbf3b67e 100644
--- a/packages/astro/test/dynamic-endpoint-collision.test.js
+++ b/packages/astro/test/dynamic-endpoint-collision.test.js
@@ -49,5 +49,10 @@ describe('Dynamic endpoint collision', () => {
 			const res = await fixture.fetch('/api/catch/one').then((r) => r.text());
 			assert.equal(res, '{"slug":"one"}');
 		});
+
+		it('returns 404 when user visits dynamic endpoint that has collision but not specified in getStaticPaths', async () => {
+			const res = await fixture.fetch('/api/safe');
+			assert.equal(res.status, 404);
+		});
 	});
 });
diff --git a/packages/astro/test/fixtures/dynamic-endpoint-collision/src/pages/api/safe/[...slug].ts b/packages/astro/test/fixtures/dynamic-endpoint-collision/src/pages/api/safe/[...slug].ts
new file mode 100644
index 0000000000..98fecec1cc
--- /dev/null
+++ b/packages/astro/test/fixtures/dynamic-endpoint-collision/src/pages/api/safe/[...slug].ts
@@ -0,0 +1,14 @@
+import type { APIRoute } from 'astro';
+
+// No undefined so should not error
+const slugs = ['one'];
+
+export const GET: APIRoute = ({ params }) => {
+	return Response.json({
+		slug: params.slug || 'index',
+	});
+};
+
+export function getStaticPaths() {
+	return slugs.map((u) => ({ params: { slug: u } }));
+}