diff --git a/src/app.ts b/src/app.ts index 809e478c887..d015f543d3e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -92,7 +92,7 @@ async function listenOnFreePort( options: ListenOptions, handler: ( request: Request, - info?: Deno.ServeHandlerInfo, + info: Deno.ServeHandlerInfo, ) => Promise, ) { // No port specified, check for a free port. Instead of picking just @@ -145,6 +145,8 @@ export class App { constructor(config: FreshConfig = {}) { this.config = normalizeConfig(config); + // Needed to make `FakeServer` work + this.handler = this.handler.bind(this); } island( @@ -243,85 +245,66 @@ export class App { return this; } - handler(): ( + async handler( request: Request, - info?: Deno.ServeHandlerInfo, - ) => Promise { - if (this.#buildCache === null) { - this.#buildCache = ProdBuildCache.fromSnapshot( - this.config, - this.#islandRegistry.size, - ); - } + info: Deno.ServeHandlerInfo, + ): Promise { + const url = new URL(request.url); + // Prevent open redirect attacks + url.pathname = url.pathname.replace(/\/+/g, "/"); + + const method = request.method.toUpperCase() as Method; + const matched = this.#router.match(method, url); + + const next = matched.patternMatch && !matched.methodMatch + ? DEFAULT_NOT_ALLOWED_METHOD + : DEFAULT_NOT_FOUND; + + const { params, handlers, pattern } = matched; + const ctx = new FreshReqContext( + request, + url, + info, + params, + this.config, + next, + this.#islandRegistry, + this.#buildCache!, + ); - if ( - !this.#buildCache.hasSnapshot && this.config.mode === "production" && - DENO_DEPLOYMENT_ID !== undefined - ) { - return missingBuildHandler; + const span = trace.getActiveSpan(); + if (span && pattern) { + span.updateName(`${method} ${pattern}`); + span.setAttribute("http.route", pattern); } - return async ( - req: Request, - conn: Deno.ServeHandlerInfo = DEFAULT_CONN_INFO, - ) => { - const url = new URL(req.url); - // Prevent open redirect attacks - url.pathname = url.pathname.replace(/\/+/g, "/"); - - const method = req.method.toUpperCase() as Method; - const matched = this.#router.match(method, url); - - const next = matched.patternMatch && !matched.methodMatch - ? DEFAULT_NOT_ALLOWED_METHOD - : DEFAULT_NOT_FOUND; - - const { params, handlers, pattern } = matched; - const ctx = new FreshReqContext( - req, - url, - conn, - params, - this.config, - next, - this.#islandRegistry, - this.#buildCache!, - ); - - const span = trace.getActiveSpan(); - if (span && pattern) { - span.updateName(`${method} ${pattern}`); - span.setAttribute("http.route", pattern); + try { + let result: unknown; + if (handlers.length === 1 && handlers[0].length === 1) { + result = await handlers[0][0](ctx); + } else { + result = await runMiddlewares(handlers, ctx); + } + if (!(result instanceof Response)) { + throw new Error( + `Expected a "Response" instance to be returned, but got: ${result}`, + ); } - try { - let result: unknown; - if (handlers.length === 1 && handlers[0].length === 1) { - result = await handlers[0][0](ctx); - } else { - result = await runMiddlewares(handlers, ctx); - } - if (!(result instanceof Response)) { - throw new Error( - `Expected a "Response" instance to be returned, but got: ${result}`, - ); - } - - return result; - } catch (err) { - if (err instanceof HttpError) { - if (err.status >= 500) { - // deno-lint-ignore no-console - console.error(err); - } - return new Response(err.message, { status: err.status }); + return result; + } catch (err) { + if (err instanceof HttpError) { + if (err.status >= 500) { + // deno-lint-ignore no-console + console.error(err); } - - // deno-lint-ignore no-console - console.error(err); - return new Response("Internal server error", { status: 500 }); + return new Response(err.message, { status: err.status }); } - }; + + // deno-lint-ignore no-console + console.error(err); + return new Response("Internal server error", { status: 500 }); + } } async listen(options: ListenOptions = {}): Promise { @@ -329,7 +312,19 @@ export class App { options.onListen = createOnListen(this.config.basePath, options); } - const handler = await this.handler(); + if (this.#buildCache === null) { + this.#buildCache = ProdBuildCache.fromSnapshot( + this.config, + this.#islandRegistry.size, + ); + } + + const handler = + !this.#buildCache.hasSnapshot && this.config.mode === "production" && + DENO_DEPLOYMENT_ID !== undefined + ? missingBuildHandler + : this.handler; + if (options.port) { await Deno.serve(options, handler); return; diff --git a/src/app_test.tsx b/src/app_test.tsx index 42fc3229e10..cff1e49f8e7 100644 --- a/src/app_test.tsx +++ b/src/app_test.tsx @@ -15,7 +15,7 @@ Deno.test("FreshApp - .use()", async () => { }) .get("/", (ctx) => new Response(ctx.state.text)); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); expect(await res.text()).toEqual("AB"); @@ -27,7 +27,7 @@ Deno.test("FreshApp - .use() #2", async () => { .get("/foo/bar", () => new Response("ok #2")) .get("/", () => new Response("ok #3")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); expect(await res.text()).toEqual("ok #1"); @@ -40,7 +40,7 @@ Deno.test("FreshApp - .get()", async () => { .get("/", () => new Response("ok")) .get("/foo", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.get("/"); expect(await res.text()).toEqual("ok"); @@ -54,7 +54,7 @@ Deno.test("FreshApp - .get() with basePath", async () => { .get("/", () => new Response("ok")) .get("/foo", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.get("/"); expect(res.status).toEqual(404); @@ -74,7 +74,7 @@ Deno.test("FreshApp - .post()", async () => { .post("/", () => new Response("ok")) .post("/foo", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.post("/"); expect(await res.text()).toEqual("ok"); @@ -88,7 +88,7 @@ Deno.test("FreshApp - .post() with basePath", async () => { .post("/", () => new Response("ok")) .post("/foo", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.post("/"); expect(res.status).toEqual(404); @@ -108,7 +108,7 @@ Deno.test("FreshApp - .patch()", async () => { .patch("/", () => new Response("ok")) .patch("/foo", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.patch("/"); expect(await res.text()).toEqual("ok"); @@ -122,7 +122,7 @@ Deno.test("FreshApp - .patch() with basePath", async () => { .patch("/", () => new Response("ok")) .patch("/foo", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.patch("/"); expect(res.status).toEqual(404); @@ -142,7 +142,7 @@ Deno.test("FreshApp - .put()", async () => { .put("/", () => new Response("ok")) .put("/foo", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.put("/"); expect(await res.text()).toEqual("ok"); @@ -156,7 +156,7 @@ Deno.test("FreshApp - .put() with basePath", async () => { .put("/", () => new Response("ok")) .put("/foo", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.put("/"); expect(res.status).toEqual(404); @@ -176,7 +176,7 @@ Deno.test("FreshApp - .delete()", async () => { .delete("/", () => new Response("ok")) .delete("/foo", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.delete("/"); expect(await res.text()).toEqual("ok"); @@ -190,7 +190,7 @@ Deno.test("FreshApp - .delete() with basePath", async () => { .delete("/", () => new Response("ok")) .delete("/foo", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.delete("/"); expect(res.status).toEqual(404); @@ -208,7 +208,7 @@ Deno.test("FreshApp - wrong method match", async () => { .get("/", () => new Response("ok")) .post("/", () => new Response("ok")); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.put("/"); expect(res.status).toEqual(405); @@ -228,7 +228,7 @@ Deno.test("FreshApp - methods with middleware", async () => { .get("/", (ctx) => new Response(ctx.state.text)) .post("/", (ctx) => new Response(ctx.state.text)); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.get("/"); expect(await res.text()).toEqual("A"); @@ -250,7 +250,7 @@ Deno.test("FreshApp - .mountApp() compose apps", async () => { .get("/", () => new Response("ok")) .mountApp("/foo", innerApp); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.get("/"); expect(await res.text()).toEqual("ok"); @@ -275,7 +275,7 @@ Deno.test("FreshApp - .mountApp() self mount, no middleware", async () => { .get("/", () => new Response("ok")) .mountApp("/", innerApp); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.get("/"); expect(await res.text()).toEqual("ok"); @@ -306,7 +306,7 @@ Deno.test( .get("/", () => new Response("ok")) .mountApp("/", innerApp); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.get("/"); expect(await res.text()).toEqual("ok"); @@ -338,7 +338,7 @@ Deno.test( .get("/", () => new Response("ok")) .mountApp("/", innerApp); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.get("/"); expect(await res.text()).toEqual("ok"); @@ -362,7 +362,7 @@ Deno.test("FreshApp - .mountApp() self mount empty", async () => { const app = new App<{ text: string }>() .mountApp("/", innerApp); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/foo"); expect(await res.text()).toEqual("A"); @@ -385,7 +385,7 @@ Deno.test( }) .mountApp("/", innerApp); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); expect(await res.text()).toEqual("Outer_Inner"); @@ -408,7 +408,7 @@ Deno.test("FreshApp - catches errors", async () => { throw new Error("fail"); }); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); expect(res.status).toEqual(500); @@ -432,7 +432,7 @@ Deno.test.ignore("FreshApp - finish setup", async () => { }, getIslandRegistry(app).size), ); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); const text = await res.text(); expect(text).toContain("Finish setting up"); @@ -462,7 +462,7 @@ Deno.test("FreshApp - sets error on context", async () => { throw ""; }); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); await res.body?.cancel(); @@ -480,7 +480,7 @@ Deno.test("FreshApp - support setting request init in ctx.render()", async () => }); }); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); await res.body?.cancel(); expect(res.status).toEqual(416); @@ -495,7 +495,7 @@ Deno.test("FreshApp - throw when middleware returns no response", async () => { (() => {}) as any, ); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); const text = await res.text(); expect(res.status).toEqual(500); diff --git a/src/context.ts b/src/context.ts index 49d6d91c9a7..d29f7b18496 100644 --- a/src/context.ts +++ b/src/context.ts @@ -97,7 +97,7 @@ export class FreshReqContext state: State = {} as State; data: unknown = undefined; error: unknown | null = null; - info: Deno.ServeHandlerInfo | Deno.ServeHandlerInfo; + info: Deno.ServeHandlerInfo; next: FreshContext["next"]; diff --git a/src/context_test.tsx b/src/context_test.tsx index 692ce01f833..29dc9669996 100644 --- a/src/context_test.tsx +++ b/src/context_test.tsx @@ -38,7 +38,7 @@ Deno.test("render asset()", async () => { , )); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); const doc = parseHtml(await res.text()); @@ -52,7 +52,7 @@ Deno.test("ctx.render - throw with no arguments", async () => { const app = new App() // deno-lint-ignore no-explicit-any .get("/", (ctx) => (ctx as any).render()); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); await res.body?.cancel(); @@ -63,7 +63,7 @@ Deno.test("ctx.render - throw with invalid first arg", async () => { const app = new App() // deno-lint-ignore no-explicit-any .get("/", (ctx) => (ctx as any).render({})); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); await res.body?.cancel(); diff --git a/src/dev/middlewares/error_overlay/middleware_test.tsx b/src/dev/middlewares/error_overlay/middleware_test.tsx index ba72b8c3036..32f6b0a4fe1 100644 --- a/src/dev/middlewares/error_overlay/middleware_test.tsx +++ b/src/dev/middlewares/error_overlay/middleware_test.tsx @@ -13,7 +13,7 @@ Deno.test("error overlay - show when error is thrown", async () => { throw new Error("fail"); }); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/", { headers: { accept: "text/html", @@ -36,7 +36,7 @@ Deno.test("error overlay - should not be visible for HttpError <500", async () = throw new HttpError(500); }); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.get("/", { headers: { accept: "text/html", @@ -74,7 +74,7 @@ Deno.test( throw new HttpError(404); }); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/", { headers: { accept: "text/html", @@ -94,7 +94,7 @@ Deno.test("error overlay - should not be visible in prod", async () => { throw new HttpError(500); }); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/", { headers: { accept: "text/html", diff --git a/src/error.ts b/src/error.ts index 610b09ba873..f659b34baac 100644 --- a/src/error.ts +++ b/src/error.ts @@ -17,10 +17,8 @@ import { STATUS_TEXT } from "@std/http/status"; * throw new HttpError(404, "Nothing here"); * }); * - * const handler = app.handler(); - * * try { - * await handler(new Request("http://localhost/not-found")) + * await app.handler(new Request("http://localhost/not-found")) * } catch (error) { * expect(error).toBeInstanceOf(HttpError); * expect(error.status).toBe(404); @@ -43,10 +41,8 @@ export class HttpError extends Error { * throw new HttpError(404, "Nothing here"); * }); * - * const handler = app.handler(); - * * try { - * await handler(new Request("http://localhost/not-found")) + * await app.handler(new Request("http://localhost/not-found")) * } catch (error) { * expect(error).toBeInstanceOf(HttpError); * expect(error.status).toBe(404); diff --git a/src/plugins/fs_routes/mod_test.tsx b/src/plugins/fs_routes/mod_test.tsx index c1e957b4a4e..7d90d01601c 100644 --- a/src/plugins/fs_routes/mod_test.tsx +++ b/src/plugins/fs_routes/mod_test.tsx @@ -35,7 +35,7 @@ async function createServer( _fs: createFakeFs(files), } as FsRoutesOptions & TESTING_ONLY__FsRoutesOptions, ); - return new FakeServer(app.handler()); + return new FakeServer(app.handler); } Deno.test("fsRoutes - throws error when file has no exports", async () => { diff --git a/src/test_utils.ts b/src/test_utils.ts index 68e678cc0f8..c23ddf14250 100644 --- a/src/test_utils.ts +++ b/src/test_utils.ts @@ -12,38 +12,38 @@ export class FakeServer { public handler: ( req: Request, info: Deno.ServeHandlerInfo, - ) => Response | Promise, + ) => Promise, ) {} - async get(path: string, init?: RequestInit): Promise { + get(path: string, init?: RequestInit): Promise { const url = this.toUrl(path); const req = new Request(url, init); - return await this.handler(req, STUB); + return this.handler(req, STUB); } - async post(path: string, body?: BodyInit): Promise { + post(path: string, body?: BodyInit): Promise { const url = this.toUrl(path); const req = new Request(url, { method: "post", body }); - return await this.handler(req, STUB); + return this.handler(req, STUB); } - async patch(path: string, body?: BodyInit): Promise { + patch(path: string, body?: BodyInit): Promise { const url = this.toUrl(path); const req = new Request(url, { method: "patch", body }); - return await this.handler(req, STUB); + return this.handler(req, STUB); } - async put(path: string, body?: BodyInit): Promise { + put(path: string, body?: BodyInit): Promise { const url = this.toUrl(path); const req = new Request(url, { method: "put", body }); - return await this.handler(req, STUB); + return this.handler(req, STUB); } - async delete(path: string): Promise { + delete(path: string): Promise { const url = this.toUrl(path); const req = new Request(url, { method: "delete" }); - return await this.handler(req, STUB); + return this.handler(req, STUB); } - async head(path: string): Promise { + head(path: string): Promise { const url = this.toUrl(path); const req = new Request(url, { method: "head" }); - return await this.handler(req, STUB); + return this.handler(req, STUB); } private toUrl(path: string) { diff --git a/tests/active_links_test.tsx b/tests/active_links_test.tsx index b522aed09cb..6cab31396f0 100644 --- a/tests/active_links_test.tsx +++ b/tests/active_links_test.tsx @@ -66,7 +66,7 @@ Deno.test({ return ctx.render(); }); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let res = await server.get("/active_nav"); let doc = parseHtml(await res.text()); diff --git a/tests/fixture_precompile/valid/main.tsx b/tests/fixture_precompile/valid/main.tsx index 7beef0698fd..0dde3a57e6c 100644 --- a/tests/fixture_precompile/valid/main.tsx +++ b/tests/fixture_precompile/valid/main.tsx @@ -1,4 +1,5 @@ import { App } from "../../../src/app.ts"; +import { DEFAULT_CONN_INFO } from "../../../src/app.ts"; const app = new App({ staticDir: "./static" }).get( "/", @@ -25,7 +26,7 @@ const app = new App({ staticDir: "./static" }).get( ), ); -const handler = app.handler(); -const res = await handler(new Request("http://localhost/")); +const handler = app.handler; +const res = await handler(new Request("http://localhost/"), DEFAULT_CONN_INFO); // deno-lint-ignore no-console console.log(await res.text()); diff --git a/tests/islands_test.tsx b/tests/islands_test.tsx index f1022f4f2aa..36eeb382e67 100644 --- a/tests/islands_test.tsx +++ b/tests/islands_test.tsx @@ -718,7 +718,7 @@ Deno.test({ , )); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); await res.body?.cancel(); diff --git a/tests/partials_test.tsx b/tests/partials_test.tsx index fe97222b671..63c093782f7 100644 --- a/tests/partials_test.tsx +++ b/tests/partials_test.tsx @@ -188,7 +188,7 @@ Deno.test({ ); }); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let checked = false; try { const res = await server.get("/"); @@ -306,7 +306,7 @@ Deno.test({ }); await buildProd(app); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); let checked = false; try { const res = await server.get("/"); @@ -845,7 +845,7 @@ Deno.test({ ); }); - const server = new FakeServer(app.handler()); + const server = new FakeServer(app.handler); const res = await server.get("/"); const html = await res.text(); diff --git a/tests/test_utils.tsx b/tests/test_utils.tsx index f98274a2aa7..5e8dd0a8e79 100644 --- a/tests/test_utils.tsx +++ b/tests/test_utils.tsx @@ -84,7 +84,7 @@ export async function withBrowserApp( port: 0, signal: aborter.signal, onListen: () => {}, // Don't spam terminal with "Listening on..." - }, app.handler()); + }, app.handler); const browser = await launch({ args: [ diff --git a/www/main_test.ts b/www/main_test.ts index 87ad4d1b689..983216adf93 100644 --- a/www/main_test.ts +++ b/www/main_test.ts @@ -3,13 +3,13 @@ import { app } from "./main.ts"; import { buildProd, withBrowserApp } from "../tests/test_utils.tsx"; import { expect } from "@std/expect"; import { retry } from "@std/async/retry"; +import { DEFAULT_CONN_INFO } from "../src/app.ts"; await buildProd(app); -const handler = app.handler(); Deno.test("CORS should not set on GET /fresh-badge.svg", async () => { const req = new Request("http://localhost/fresh-badge.svg"); - const resp = await handler(req); + const resp = await app.handler(req, DEFAULT_CONN_INFO); await resp?.body?.cancel(); expect(resp.headers.get("cross-origin-resource-policy")).toEqual(null);