diff --git a/packages/start/api/index.ts b/packages/start/api/index.ts index 398dce323..2e81be321 100644 --- a/packages/start/api/index.ts +++ b/packages/start/api/index.ts @@ -8,6 +8,12 @@ import { MatchRoute, Method, Route } from "./types"; // @ts-ignore var api = $API_ROUTES; +// used by the compiled configuration of routes +// @ts-ignore +function methodNotAllowed() { + return new Response(null, { status: 405 }); +} + // This is copied from https://github.com/solidjs/solid-router/blob/main/src/utils.ts function expandOptionals(pattern: string): string[] { let match = /(\/?\:[^\/]+)\?/.exec(pattern); @@ -95,3 +101,4 @@ export function isApiRequest(request: Request) { export * from "../server/responses"; export type { APIEvent } from "./types"; + diff --git a/packages/start/entry-server/index.ts b/packages/start/entry-server/index.ts index 25a1c2a46..648c79dde 100644 --- a/packages/start/entry-server/index.ts +++ b/packages/start/entry-server/index.ts @@ -1,4 +1,4 @@ -export { composeMiddleware, createHandler, default as StartServer } from "./StartServer"; +export { default as StartServer, composeMiddleware, createHandler } from "./StartServer"; export type { Middleware, MiddlewareFn, MiddlewareInput } from "./StartServer"; import { JSX } from "solid-js"; @@ -21,8 +21,8 @@ export const render = ( } ) => composeMiddleware([ - apiRoutes, inlineServerFunctions, + apiRoutes, import.meta.env.START_SSR === "async" ? _renderAsync(fn, options) : import.meta.env.START_SSR === "streaming" @@ -37,7 +37,7 @@ export const renderAsync = ( nonce?: string; renderId?: string; } -) => composeMiddleware([apiRoutes, inlineServerFunctions, _renderAsync(fn, options)]); +) => composeMiddleware([inlineServerFunctions, apiRoutes, _renderAsync(fn, options)]); export const renderStream = ( fn: (context: PageEvent) => JSX.Element, @@ -46,7 +46,7 @@ export const renderStream = ( nonce?: string; renderId?: string; } -) => composeMiddleware([apiRoutes, inlineServerFunctions, _renderStream(fn, options)]); +) => composeMiddleware([inlineServerFunctions, apiRoutes, _renderStream(fn, options)]); export const renderSync = ( fn: (context: PageEvent) => JSX.Element, @@ -55,4 +55,4 @@ export const renderSync = ( nonce?: string; renderId?: string; } -) => composeMiddleware([apiRoutes, inlineServerFunctions, _renderSync(fn, options)]); +) => composeMiddleware([inlineServerFunctions, apiRoutes, _renderSync(fn, options)]); diff --git a/packages/start/fs-router/router.js b/packages/start/fs-router/router.js index f98c16d7d..c1148372b 100644 --- a/packages/start/fs-router/router.js +++ b/packages/start/fs-router/router.js @@ -379,7 +379,7 @@ export function stringifyPageRoutes( export function stringifyAPIRoutes( /** @type {(RouteConfig)[]} */ flatRoutes, - /** @type {{ lazy?: boolean }} */ options = {} + /** @type {{ lazy?: boolean, islandsRouter?: boolean }} */ options = {} ) { const jsFile = jsCode(); @@ -393,12 +393,15 @@ export function stringifyAPIRoutes( .map( i => `{\n${[ - ...API_METHODS.filter(j => i.apiPath?.[j]).map( - v => - i.apiPath != null && - `${v}: ${jsFile.addNamedImport(v, path.posix.resolve(i.apiPath[v]))}` - ), - i.componentPath ? `GET: "skip"` : undefined, + ...API_METHODS.map(v => { + if (v === "GET" && i.componentPath) + return `${v}: "skip"`; + if (v === "POST" && options.islandsRouter && i.componentPath) + return `${v}: "skip"`; + else if (i.apiPath?.[v]) + return `${v}: ${jsFile.addNamedImport(v, path.posix.resolve(i.apiPath[v]))}`; + return `${v}: methodNotAllowed`; + }), `path: ${JSON.stringify(i.path)}` ] .filter(Boolean) diff --git a/packages/start/vite/plugin.js b/packages/start/vite/plugin.js index 59afaa670..c01c4bfe2 100644 --- a/packages/start/vite/plugin.js +++ b/packages/start/vite/plugin.js @@ -440,7 +440,10 @@ function solidStartFileSystemRouter(options) { return { code: code.replace( "var api = $API_ROUTES;", - stringifyAPIRoutes(config.solidOptions.router.getFlattenedApiRoutes(true), { lazy }) + stringifyAPIRoutes( + config.solidOptions.router.getFlattenedApiRoutes(true), + { lazy, islandsRouter: !!config.solidOptions.experimental?.islandsRouter } + ) ) }; } diff --git a/test/api-routes-test.ts b/test/api-routes-test.ts index 260555751..358e7530e 100644 --- a/test/api-routes-test.ts +++ b/test/api-routes-test.ts @@ -131,6 +131,12 @@ test.describe("api routes", () => { "src/routes/api/[param]/index.js": js` import { json } from "solid-start/server"; export let GET = ({ params }) => json(params); + `, + "src/routes/method-not-allowed.jsx": js` + export default function Page() { return
page
; } + `, + "src/routes/api/method-not-allowed.js": js` + export function POST () { return new Response(); } ` } }); @@ -280,5 +286,23 @@ test.describe("api routes", () => { expect(res.headers.get("content-type")).toEqual("application/json; charset=utf-8"); expect(await res.json()).toEqual({ static: true }); }); + + test("should return 405 for undefined handlers on route with only a default export", async () => { + let res = await fixture.requestDocument("/method-not-allowed", { method: "GET" }); + expect(res.status).toEqual(200); + ["POST", "PUT", "PATCH", "DELETE"].forEach(async method => { + res = await fixture.requestDocument("/method-not-allowed", { method }); + expect(res.status).toEqual(405); + }) + }); + + test("should return 405 for undefined handlers on route with only a POST export", async () => { + let res = await fixture.requestDocument("/api/method-not-allowed", { method: "POST" }); + expect(res.status).toEqual(200); + ["GET", "PUT", "PATCH", "DELETE"].forEach(async method => { + res = await fixture.requestDocument("/api/method-not-allowed", { method }); + expect(res.status).toEqual(405); + }); + }); } });