-
-
Notifications
You must be signed in to change notification settings - Fork 619
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(cloudflare-pages): Add Cloudflare Pages middleware handler #3028
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,8 @@ | ||
import { getCookie } from '../../helper/cookie' | ||
import { Hono } from '../../hono' | ||
import { HTTPException } from '../../http-exception' | ||
import type { EventContext } from './handler' | ||
import { handle } from './handler' | ||
import { handle, handleMiddleware } from './handler' | ||
|
||
type Env = { | ||
Bindings: { | ||
|
@@ -49,3 +51,193 @@ 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') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Included this as an example of my use-case, but it's not entirely necessary as part of the test |
||
|
||
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 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 = { | ||
TOKEN: 'HONOISCOOL', | ||
} | ||
const handler = handleMiddleware(async (c) => { | ||
return c.json({ response: 'Skip Cloudflare Pages' }) | ||
}) | ||
|
||
const next = vi.fn() | ||
// 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((c, next) => 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') | ||
}) | ||
|
||
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() | ||
// 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 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(() => { | ||
throw new Error('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') | ||
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') | ||
expect(next).not.toHaveBeenCalled() | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,7 @@ | ||
import { Context } from '../../context' | ||
import type { Hono } from '../../hono' | ||
import type { MiddlewareHandler } from '../../types' | ||
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 | ||
|
||
|
@@ -18,6 +20,13 @@ export type EventContext<Env = {}, P extends string = any, Data = {}> = { | |
data: Data | ||
} | ||
|
||
declare type PagesFunction< | ||
Env = unknown, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
Params extends string = any, | ||
Data extends Record<string, unknown> = Record<string, unknown> | ||
> = (context: EventContext<Env, Params, Data>) => Response | Promise<Response> | ||
|
||
interface HandleInterface { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(app: Hono<any, any, any>): (eventContext: EventContext) => Response | Promise<Response> | ||
|
@@ -34,6 +43,54 @@ export const handle: HandleInterface = (app: Hono) => (eventContext: EventContex | |
) | ||
} | ||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export function handleMiddleware<E extends Env = any, P extends string = any, I extends Input = {}>( | ||
BarryThePenguin marked this conversation as resolved.
Show resolved
Hide resolved
|
||
middleware: MiddlewareHandler<E, P, I> | ||
): PagesFunction { | ||
return async (executionCtx) => { | ||
const context = new Context(executionCtx.request, { | ||
env: executionCtx.env, | ||
executionCtx, | ||
}) | ||
|
||
let response: Response | void = undefined | ||
|
||
try { | ||
response = await middleware(context, async () => { | ||
try { | ||
context.res = await executionCtx.next() | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
context.error = error | ||
} else { | ||
throw error | ||
} | ||
} | ||
}) | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
context.error = error | ||
} else { | ||
throw error | ||
} | ||
} | ||
|
||
if (response) { | ||
return response | ||
} | ||
|
||
if (context.error instanceof HTTPException) { | ||
return context.error.getResponse() | ||
} | ||
|
||
if (context.error) { | ||
throw context.error | ||
} | ||
|
||
return context.res | ||
} | ||
Comment on lines
+58
to
+91
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm still not sure all of this is necessary.. I added all the test cases I could think of, so hopefully all the main use cases are covered |
||
} | ||
|
||
declare abstract class FetcherLike { | ||
fetch(input: RequestInfo, init?: RequestInit): Promise<Response> | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like
next
is failing due toHonoRequest
being removed 👀