Skip to content

Commit

Permalink
feat(cloudflare-pages): Add Cloudflare Pages middleware handler (#3028)
Browse files Browse the repository at this point in the history
* Add Cloudflare Pages middleware handler

* fix: handle HTTPException

* fix: handle context.error

* fix: remove HonoRequest from conninfo test
  • Loading branch information
BarryThePenguin authored Jun 29, 2024
1 parent a8a84f3 commit 204e10b
Show file tree
Hide file tree
Showing 3 changed files with 252 additions and 3 deletions.
194 changes: 193 additions & 1 deletion src/adapter/cloudflare-pages/handler.test.ts
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: {
Expand Down Expand Up @@ -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()
})
})
59 changes: 58 additions & 1 deletion src/adapter/cloudflare-pages/handler.ts
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

Expand All @@ -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>
Expand All @@ -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>
}
Expand Down
2 changes: 1 addition & 1 deletion src/adapter/cloudflare-pages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

0 comments on commit 204e10b

Please sign in to comment.