From 5b79c73b2d9a33973184b8d7088c309e26acedf4 Mon Sep 17 00:00:00 2001 From: Jonathan Haines Date: Mon, 24 Jun 2024 08:47:35 +1000 Subject: [PATCH 1/4] Add Cloudflare Pages middleware handler --- src/adapter/cloudflare-pages/handler.test.ts | 75 +++++++++++++++++++- src/adapter/cloudflare-pages/handler.ts | 29 +++++++- 2 files changed, 102 insertions(+), 2 deletions(-) diff --git a/src/adapter/cloudflare-pages/handler.test.ts b/src/adapter/cloudflare-pages/handler.test.ts index c6e1443ae..d681a1e4a 100644 --- a/src/adapter/cloudflare-pages/handler.test.ts +++ b/src/adapter/cloudflare-pages/handler.test.ts @@ -1,6 +1,7 @@ +import { getCookie } from '../../helper/cookie' import { Hono } from '../../hono' import type { EventContext } from './handler' -import { handle } from './handler' +import { handle, handleMiddleware } from './handler' type Env = { Bindings: { @@ -49,3 +50,75 @@ describe('Adapter for Cloudflare Pages', () => { expect(() => handler({ request })).toThrowError('Custom Error') }) }) + +describe('Middleware adapter for Cloudflare Pages', () => { + it('Should return the middleware response', async () => { + const request = new Request('http://localhost/api/foo', { + headers: { + Cookie: 'my_cookie=1234', + }, + }) + const env = { + TOKEN: 'HONOISCOOL', + } + const handler = handleMiddleware(async (c, next) => { + const cookie = getCookie(c, 'my_cookie') + + await next() + + return c.json({ cookie, response: await c.res.json() }) + }) + + const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const res = await handler({ request, env, next }) + + expect(next).toHaveBeenCalled() + + expect(await res.json()).toEqual({ + cookie: '1234', + response: 'From Cloudflare Pages', + }) + }) + + it('Should return the middleware response if next() is not called', async () => { + const request = new Request('http://localhost/api/foo') + const env = { + TOKEN: 'HONOISCOOL', + } + const handler = handleMiddleware(async (c) => { + return c.json({ response: 'Skip Cloudflare Pages' }) + }) + + const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const res = await handler({ request, env, next }) + + expect(next).not.toHaveBeenCalled() + + expect(await res.json()).toEqual({ + response: 'Skip Cloudflare Pages', + }) + }) + + it('Should return the Pages response if the middleware does not return a response', async () => { + const request = new Request('http://localhost/api/foo') + const env = { + TOKEN: 'HONOISCOOL', + } + const handler = handleMiddleware(async (c, next) => { + await next() + }) + + const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const res = await handler({ request, env, next }) + + expect(next).toHaveBeenCalled() + + expect(await res.json()).toEqual('From Cloudflare Pages') + }) +}) diff --git a/src/adapter/cloudflare-pages/handler.ts b/src/adapter/cloudflare-pages/handler.ts index 7e3f25e1e..8d3325aa0 100644 --- a/src/adapter/cloudflare-pages/handler.ts +++ b/src/adapter/cloudflare-pages/handler.ts @@ -1,5 +1,7 @@ +import { Context } from '../../context' import type { Hono } from '../../hono' -import type { MiddlewareHandler } from '../../types' +import { HonoRequest } from '../../request' +import type { Env, Input, MiddlewareHandler } from '../../types' // Ref: https://github.com/cloudflare/workerd/blob/main/types/defines/pages.d.ts @@ -18,6 +20,13 @@ export type EventContext = { data: Data } +declare type PagesFunction< + Env = unknown, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + Params extends string = any, + Data extends Record = Record +> = (context: EventContext) => Response | Promise + interface HandleInterface { // eslint-disable-next-line @typescript-eslint/no-explicit-any (app: Hono): (eventContext: EventContext) => Response | Promise @@ -34,6 +43,24 @@ export const handle: HandleInterface = (app: Hono) => (eventContext: EventContex ) } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function handleMiddleware( + middleware: MiddlewareHandler +): PagesFunction { + return async (executionCtx) => { + const context = new Context(new HonoRequest(executionCtx.request), { + env: executionCtx.env, + executionCtx, + }) + + const response = await middleware(context, async () => { + context.res = await executionCtx.next() + }) + + return response ?? context.res + } +} + declare abstract class FetcherLike { fetch(input: RequestInfo, init?: RequestInit): Promise } From 2d8d200dbcd652af8003ba12b53eea826c991789 Mon Sep 17 00:00:00 2001 From: Jonathan Haines Date: Thu, 27 Jun 2024 20:12:54 +1000 Subject: [PATCH 2/4] fix: handle HTTPException --- src/adapter/cloudflare-pages/handler.test.ts | 38 ++++++++++++++++++++ src/adapter/cloudflare-pages/handler.ts | 20 ++++++++--- src/adapter/cloudflare-pages/index.ts | 2 +- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/src/adapter/cloudflare-pages/handler.test.ts b/src/adapter/cloudflare-pages/handler.test.ts index d681a1e4a..39113cc98 100644 --- a/src/adapter/cloudflare-pages/handler.test.ts +++ b/src/adapter/cloudflare-pages/handler.test.ts @@ -1,5 +1,6 @@ import { getCookie } from '../../helper/cookie' import { Hono } from '../../hono' +import { HTTPException } from '../../http-exception' import type { EventContext } from './handler' import { handle, handleMiddleware } from './handler' @@ -121,4 +122,41 @@ describe('Middleware adapter for Cloudflare Pages', () => { expect(await res.json()).toEqual('From Cloudflare Pages') }) + + it('Should handle a HTTPException by returning error.getResponse()', async () => { + const request = new Request('http://localhost/api/foo') + const env = { + TOKEN: 'HONOISCOOL', + } + const handler = handleMiddleware(() => { + const res = new Response('Unauthorized', { status: 401 }) + throw new HTTPException(401, { res }) + }) + + const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const res = await handler({ request, env, next }) + + expect(next).not.toHaveBeenCalled() + + expect(res.status).toBe(401) + expect(await res.text()).toBe('Unauthorized') + }) + + it('Should rethrow an Error', async () => { + const request = new Request('http://localhost/api/foo') + const env = { + TOKEN: 'HONOISCOOL', + } + const handler = handleMiddleware(async () => { + throw new Error('Something went wrong') + }) + + const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await expect(handler({ request, env, next })).rejects.toThrowError('Something went wrong') + expect(next).not.toHaveBeenCalled() + }) }) diff --git a/src/adapter/cloudflare-pages/handler.ts b/src/adapter/cloudflare-pages/handler.ts index 8d3325aa0..b6870946d 100644 --- a/src/adapter/cloudflare-pages/handler.ts +++ b/src/adapter/cloudflare-pages/handler.ts @@ -1,6 +1,6 @@ import { Context } from '../../context' import type { Hono } from '../../hono' -import { HonoRequest } from '../../request' +import { HTTPException } from '../../http-exception' import type { Env, Input, MiddlewareHandler } from '../../types' // Ref: https://github.com/cloudflare/workerd/blob/main/types/defines/pages.d.ts @@ -48,14 +48,24 @@ export function handleMiddleware ): PagesFunction { return async (executionCtx) => { - const context = new Context(new HonoRequest(executionCtx.request), { + const context = new Context(executionCtx.request, { env: executionCtx.env, executionCtx, }) - const response = await middleware(context, async () => { - context.res = await executionCtx.next() - }) + let response: Response | void = undefined + + try { + response = await middleware(context, async () => { + context.res = await executionCtx.next() + }) + } catch (error) { + if (error instanceof HTTPException) { + response = error.getResponse() + } else { + throw error + } + } return response ?? context.res } diff --git a/src/adapter/cloudflare-pages/index.ts b/src/adapter/cloudflare-pages/index.ts index e5362c817..0bbeb2a37 100644 --- a/src/adapter/cloudflare-pages/index.ts +++ b/src/adapter/cloudflare-pages/index.ts @@ -3,5 +3,5 @@ * Cloudflare Pages Adapter for Hono. */ -export { handle, serveStatic } from './handler' +export { handle, handleMiddleware, serveStatic } from './handler' export type { EventContext } from './handler' From 2db0eeb264e839634de4da44e027ce06943d4a59 Mon Sep 17 00:00:00 2001 From: Jonathan Haines Date: Fri, 28 Jun 2024 10:58:04 +1000 Subject: [PATCH 3/4] fix: handle context.error --- src/adapter/cloudflare-pages/handler.test.ts | 95 ++++++++++++++++++-- src/adapter/cloudflare-pages/handler.ts | 28 +++++- 2 files changed, 112 insertions(+), 11 deletions(-) diff --git a/src/adapter/cloudflare-pages/handler.test.ts b/src/adapter/cloudflare-pages/handler.test.ts index 39113cc98..8f57b7c8c 100644 --- a/src/adapter/cloudflare-pages/handler.test.ts +++ b/src/adapter/cloudflare-pages/handler.test.ts @@ -83,6 +83,29 @@ describe('Middleware adapter for Cloudflare Pages', () => { }) }) + it('Should return the middleware response when exceptions are handled', async () => { + const request = new Request('http://localhost/api/foo') + const env = { + TOKEN: 'HONOISCOOL', + } + const handler = handleMiddleware(async (c, next) => { + await next() + + return c.json({ error: c.error?.message }) + }) + + const next = vi.fn().mockRejectedValue(new Error('Error from next()')) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const res = await handler({ request, env, next }) + + expect(next).toHaveBeenCalled() + + expect(await res.json()).toEqual({ + error: 'Error from next()', + }) + }) + it('Should return the middleware response if next() is not called', async () => { const request = new Request('http://localhost/api/foo') const env = { @@ -92,7 +115,7 @@ describe('Middleware adapter for Cloudflare Pages', () => { return c.json({ response: 'Skip Cloudflare Pages' }) }) - const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) + const next = vi.fn() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const res = await handler({ request, env, next }) @@ -109,9 +132,7 @@ describe('Middleware adapter for Cloudflare Pages', () => { const env = { TOKEN: 'HONOISCOOL', } - const handler = handleMiddleware(async (c, next) => { - await next() - }) + const handler = handleMiddleware((c, next) => next()) const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -133,7 +154,7 @@ describe('Middleware adapter for Cloudflare Pages', () => { throw new HTTPException(401, { res }) }) - const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) + const next = vi.fn() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const res = await handler({ request, env, next }) @@ -144,16 +165,76 @@ describe('Middleware adapter for Cloudflare Pages', () => { expect(await res.text()).toBe('Unauthorized') }) + it('Should handle an HTTPException thrown by next()', async () => { + const request = new Request('http://localhost/api/foo') + const env = { + TOKEN: 'HONOISCOOL', + } + const handler = handleMiddleware((c, next) => next()) + + const next = vi + .fn() + .mockRejectedValue(new HTTPException(401, { res: Response.json('Unauthorized') })) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const res = await handler({ request, env, next }) + + expect(next).toHaveBeenCalled() + + expect(await res.json()).toEqual('Unauthorized') + }) + + it('Should handle an Error thrown by next()', async () => { + const request = new Request('http://localhost/api/foo') + const env = { + TOKEN: 'HONOISCOOL', + } + const handler = handleMiddleware((c, next) => next()) + + const next = vi.fn().mockRejectedValue(new Error('Error from next()')) + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await expect(handler({ request, env, next })).rejects.toThrowError('Error from next()') + expect(next).toHaveBeenCalled() + }) + + it('Should handle a non-Error thrown by next()', async () => { + const request = new Request('http://localhost/api/foo') + const env = { + TOKEN: 'HONOISCOOL', + } + const handler = handleMiddleware((c, next) => next()) + + const next = vi.fn().mockRejectedValue('Error from next()') + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await expect(handler({ request, env, next })).rejects.toThrowError('Error from next()') + expect(next).toHaveBeenCalled() + }) + it('Should rethrow an Error', async () => { const request = new Request('http://localhost/api/foo') const env = { TOKEN: 'HONOISCOOL', } - const handler = handleMiddleware(async () => { + const handler = handleMiddleware(() => { throw new Error('Something went wrong') }) - const next = vi.fn().mockResolvedValue(Response.json('From Cloudflare Pages')) + const next = vi.fn() + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + await expect(handler({ request, env, next })).rejects.toThrowError('Something went wrong') + expect(next).not.toHaveBeenCalled() + }) + + it('Should rethrow non-Error exceptions', async () => { + const request = new Request('http://localhost/api/foo') + const env = { + TOKEN: 'HONOISCOOL', + } + const handler = handleMiddleware(() => Promise.reject('Something went wrong')) + const next = vi.fn() // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore await expect(handler({ request, env, next })).rejects.toThrowError('Something went wrong') diff --git a/src/adapter/cloudflare-pages/handler.ts b/src/adapter/cloudflare-pages/handler.ts index b6870946d..371175b87 100644 --- a/src/adapter/cloudflare-pages/handler.ts +++ b/src/adapter/cloudflare-pages/handler.ts @@ -57,17 +57,37 @@ export function handleMiddleware { - context.res = await executionCtx.next() + try { + context.res = await executionCtx.next() + } catch (error) { + if (error instanceof Error) { + context.error = error + } else { + throw error + } + } }) } catch (error) { - if (error instanceof HTTPException) { - response = error.getResponse() + if (error instanceof Error) { + context.error = error } else { throw error } } - return response ?? context.res + if (response) { + return response + } + + if (context.error instanceof HTTPException) { + return context.error.getResponse() + } + + if (context.error) { + throw context.error + } + + return context.res } } From 7bd318f82eeeea26649784fc0d7d8e7f2bf20b25 Mon Sep 17 00:00:00 2001 From: Jonathan Haines Date: Fri, 28 Jun 2024 10:58:31 +1000 Subject: [PATCH 4/4] fix: remove HonoRequest from conninfo test --- src/adapter/bun/conninfo.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapter/bun/conninfo.test.ts b/src/adapter/bun/conninfo.test.ts index b0a5096ff..9c76512e1 100644 --- a/src/adapter/bun/conninfo.test.ts +++ b/src/adapter/bun/conninfo.test.ts @@ -49,7 +49,7 @@ describe('getConnInfo', () => { }) it('should return undefined when addressType is invalid string', () => { const { server } = createRandomBunServer({ family: 'invalid' }) - const c = new Context(new HonoRequest(new Request('http://localhost/')), { env: { server } }) + const c = new Context(new Request('http://localhost/'), { env: { server } }) const info = getConnInfo(c)