0
0
mirror of https://github.com/honojs/hono.git synced 2024-11-21 18:18:57 +01:00

feat(cloudflare-pages): Add Cloudflare Pages middleware handler (#3028)

* Add Cloudflare Pages middleware handler

* fix: handle HTTPException

* fix: handle context.error

* fix: remove HonoRequest from conninfo test
This commit is contained in:
Jonathan Haines 2024-06-29 18:35:20 +10:00 committed by GitHub
parent a8a84f3055
commit 204e10b7ce
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 252 additions and 3 deletions

View File

@ -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')
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()
})
})

View File

@ -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 = {}>(
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
}
}
declare abstract class FetcherLike {
fetch(input: RequestInfo, init?: RequestInit): Promise<Response>
}

View File

@ -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'