Skip to content
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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/adapter/bun/conninfo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 } })
Copy link
Contributor Author

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 to HonoRequest being removed 👀

const c = new Context(new Request('http://localhost/'), { env: { server } })

const info = getConnInfo(c)

Expand Down
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')
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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()
})
})
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 = {}>(
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
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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>
}
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'

Check warning on line 6 in src/adapter/cloudflare-pages/index.ts

View check run for this annotation

Codecov / codecov/patch

src/adapter/cloudflare-pages/index.ts#L6

Added line #L6 was not covered by tests
export type { EventContext } from './handler'