diff --git a/docs/latest/advanced/opentelemetry.md b/docs/latest/advanced/opentelemetry.md index 6b715ca6d20..b7b2ba639ad 100644 --- a/docs/latest/advanced/opentelemetry.md +++ b/docs/latest/advanced/opentelemetry.md @@ -114,7 +114,7 @@ with Docker: docker run -d --name jaeger \ -p 16686:16686 \ -p 4317:4317 \ - jaegertracing/all-in-one:latest + cr.jaegertracing.io/jaegertracing/jaeger:latest ``` Then start your Fresh app pointing at the Jaeger collector: diff --git a/packages/fresh/src/commands.ts b/packages/fresh/src/commands.ts index c841c9bc402..7d9c128b4b1 100644 --- a/packages/fresh/src/commands.ts +++ b/packages/fresh/src/commands.ts @@ -230,6 +230,8 @@ function applyCommandsInner( basePath: string, onError?: (err: unknown) => void, ) { + const routePhase: (() => void)[] = []; + for (let i = 0; i < commands.length; i++) { const cmd = commands[i]; @@ -274,13 +276,14 @@ function applyCommandsInner( break; } case CommandType.Route: { - const { pattern, route, config } = cmd; const segment = getOrCreateSegment( root, - pattern, + cmd.pattern, cmd.includeLastSegment, ); - const fns = segmentToMiddlewares(segment); + const { pattern, route, config } = cmd; + routePhase.push(() => { + const fns = segmentToMiddlewares(segment); if (isLazy(route)) { const routePath = mergePath( @@ -337,33 +340,35 @@ function applyCommandsInner( } } } + }); break; } case CommandType.Handler: { - const { pattern, fns, method } = cmd; const segment = getOrCreateSegment( root, - pattern, + cmd.pattern, cmd.includeLastSegment, ); - const result = segmentToMiddlewares(segment); - - result.push(...fns); + const { pattern, fns, method } = cmd; + routePhase.push(() => { + const result = segmentToMiddlewares(segment); - const compiled = compileMiddlewares(result, onError); - const resPath = toRoutePath(mergePath(basePath, pattern, false)); - if (method === "ALL") { - router.add("GET", resPath, compiled); - router.add("DELETE", resPath, compiled); - router.add("HEAD", resPath, compiled); - router.add("OPTIONS", resPath, compiled); - router.add("PATCH", resPath, compiled); - router.add("POST", resPath, compiled); - router.add("PUT", resPath, compiled); - } else { - router.add(method, resPath, compiled); - } + result.push(...fns); + const compiled = compileMiddlewares(result, onError); + const resPath = toRoutePath(mergePath(basePath, pattern, false)); + if (method === "ALL") { + router.add("GET", resPath, compiled); + router.add("DELETE", resPath, compiled); + router.add("HEAD", resPath, compiled); + router.add("OPTIONS", resPath, compiled); + router.add("PATCH", resPath, compiled); + router.add("POST", resPath, compiled); + router.add("PUT", resPath, compiled); + } else { + router.add(method, resPath, compiled); + } + }); break; } case CommandType.FsRoute: { @@ -376,4 +381,11 @@ function applyCommandsInner( throw new Error(`Unknown command: ${JSON.stringify(cmd)}`); } } + + // Phase 2: Register routes after all middlewares are in place. + // This ensures that middleware declared after routes still wraps + // those routes correctly. + for (const fn of routePhase) { + fn(); + } } diff --git a/packages/fresh/src/middlewares/csp_order_test.tsx b/packages/fresh/src/middlewares/csp_order_test.tsx new file mode 100644 index 00000000000..cd5a2ef7530 --- /dev/null +++ b/packages/fresh/src/middlewares/csp_order_test.tsx @@ -0,0 +1,47 @@ +import { expect } from "@std/expect/expect"; +import { App } from "../app.ts"; +import { csp } from "./csp.ts"; +import { FakeServer } from "../test_utils.ts"; + +Deno.test("CSP - useNonce after route still sets header", async () => { + // Reproduces #3844: middleware registered AFTER route + const app = new App() + .get("/", (ctx) => { + return ctx.render( + + +

hello

+ , + ); + }) + .use(csp({ useNonce: true })); + + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + const cspHeader = res.headers.get("Content-Security-Policy"); + + // The header should be set and should contain the nonce + expect(cspHeader).toBeDefined(); + expect(cspHeader).not.toBeNull(); + expect(cspHeader!).toMatch(/'nonce-[a-f0-9]+'/); +}); + +Deno.test("CSP - useNonce before route still sets header", async () => { + // Control: middleware registered BEFORE route (the normal case) + const app = new App() + .use(csp({ useNonce: true })) + .get("/", (ctx) => { + return ctx.render( + + +

hello

+ , + ); + }); + + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + const cspHeader = res.headers.get("Content-Security-Policy")!; + + expect(cspHeader).toMatch(/'nonce-[a-f0-9]+'/); +});