This release is 9 versions behind 4.7.9 — the latest version of @hono/hono. Jump to latest
Built and signed on GitHub ActionsBuilt and signed on GitHub Actions
Built and signed on GitHub Actions
Web framework built on Web Standards
This package works with Cloudflare Workers, Node.js, Deno, Bun, Browsers




JSR Score
76%
Published
4 months ago (4.6.20)
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503import crypto from 'node:crypto' import type { Hono } from '../../hono.ts' import type { Env, Schema } from '../../types.ts' import { decodeBase64, encodeBase64 } from '../../utils/encode.ts' import type { ALBRequestContext, ApiGatewayRequestContext, ApiGatewayRequestContextV2, Handler, LambdaContext, } from './types.ts' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore globalThis.crypto ??= crypto export type LambdaEvent = APIGatewayProxyEvent | APIGatewayProxyEventV2 | ALBProxyEvent // When calling HTTP API or Lambda directly through function urls export interface APIGatewayProxyEventV2 { version: string routeKey: string headers: Record<string, string | undefined> multiValueHeaders?: undefined cookies?: string[] rawPath: string rawQueryString: string body: string | null isBase64Encoded: boolean requestContext: ApiGatewayRequestContextV2 queryStringParameters?: { [name: string]: string | undefined } pathParameters?: { [name: string]: string | undefined } stageVariables?: { [name: string]: string | undefined } } // When calling Lambda through an API Gateway export interface APIGatewayProxyEvent { version: string httpMethod: string headers: Record<string, string | undefined> multiValueHeaders?: { [headerKey: string]: string[] } path: string body: string | null isBase64Encoded: boolean queryStringParameters?: Record<string, string | undefined> requestContext: ApiGatewayRequestContext resource: string multiValueQueryStringParameters?: { [parameterKey: string]: string[] } pathParameters?: Record<string, string> stageVariables?: Record<string, string> } // When calling Lambda through an Application Load Balancer export interface ALBProxyEvent { httpMethod: string headers?: Record<string, string | undefined> multiValueHeaders?: Record<string, string[] | undefined> path: string body: string | null isBase64Encoded: boolean queryStringParameters?: Record<string, string | undefined> multiValueQueryStringParameters?: { [parameterKey: string]: string[] } requestContext: ALBRequestContext } export interface APIGatewayProxyResult { statusCode: number statusDescription?: string body: string headers: Record<string, string> cookies?: string[] multiValueHeaders?: { [headerKey: string]: string[] } isBase64Encoded: boolean } const getRequestContext = ( event: LambdaEvent ): ApiGatewayRequestContext | ApiGatewayRequestContextV2 | ALBRequestContext => { return event.requestContext } const streamToNodeStream = async ( reader: ReadableStreamDefaultReader<Uint8Array>, writer: NodeJS.WritableStream ): Promise<void> => { let readResult = await reader.read() while (!readResult.done) { writer.write(readResult.value) readResult = await reader.read() } writer.end() } export const streamHandle = < E extends Env = Env, S extends Schema = {}, BasePath extends string = '/' >( app: Hono<E, S, BasePath> ): Handler => { // @ts-expect-error awslambda is not a standard API return awslambda.streamifyResponse( async (event: LambdaEvent, responseStream: NodeJS.WritableStream, context: LambdaContext) => { const processor = getProcessor(event) try { const req = processor.createRequest(event) const requestContext = getRequestContext(event) const res = await app.fetch(req, { event, requestContext, context, }) const headers: Record<string, string> = {} const cookies: string[] = [] res.headers.forEach((value, name) => { if (name === 'set-cookie') { cookies.push(value) } else { headers[name] = value } }) // Check content type const httpResponseMetadata = { statusCode: res.status, headers, cookies, } // Update response stream // @ts-expect-error awslambda is not a standard API responseStream = awslambda.HttpResponseStream.from(responseStream, httpResponseMetadata) if (res.body) { await streamToNodeStream(res.body.getReader(), responseStream) } else { responseStream.write('') } } catch (error) { console.error('Error processing request:', error) responseStream.write('Internal Server Error') } finally { responseStream.end() } } ) } /** * Accepts events from API Gateway/ELB(`APIGatewayProxyEvent`) and directly through Function Url(`APIGatewayProxyEventV2`) */ export const handle = <E extends Env = Env, S extends Schema = {}, BasePath extends string = '/'>( app: Hono<E, S, BasePath> ): ((event: LambdaEvent, lambdaContext?: LambdaContext) => Promise<APIGatewayProxyResult>) => { return async (event, lambdaContext?) => { const processor = getProcessor(event) const req = processor.createRequest(event) const requestContext = getRequestContext(event) const res = await app.fetch(req, { event, requestContext, lambdaContext, }) return processor.createResult(event, res) } } export abstract class EventProcessor<E extends LambdaEvent> { protected abstract getPath(event: E): string protected abstract getMethod(event: E): string protected abstract getQueryString(event: E): string protected abstract getHeaders(event: E): Headers protected abstract getCookies(event: E, headers: Headers): void protected abstract setCookiesToResult( event: E, result: APIGatewayProxyResult, cookies: string[] ): void createRequest(event: E): Request { const queryString = this.getQueryString(event) const domainName = event.requestContext && 'domainName' in event.requestContext ? event.requestContext.domainName : event.headers?.['host'] ?? event.multiValueHeaders?.['host']?.[0] const path = this.getPath(event) const urlPath = `https://${domainName}${path}` const url = queryString ? `${urlPath}?${queryString}` : urlPath const headers = this.getHeaders(event) const method = this.getMethod(event) const requestInit: RequestInit = { headers, method, } if (event.body) { requestInit.body = event.isBase64Encoded ? decodeBase64(event.body) : event.body } return new Request(url, requestInit) } async createResult(event: E, res: Response): Promise<APIGatewayProxyResult> { const contentType = res.headers.get('content-type') let isBase64Encoded = contentType && isContentTypeBinary(contentType) ? true : false if (!isBase64Encoded) { const contentEncoding = res.headers.get('content-encoding') isBase64Encoded = isContentEncodingBinary(contentEncoding) } const body = isBase64Encoded ? encodeBase64(await res.arrayBuffer()) : await res.text() const result: APIGatewayProxyResult = { body: body, headers: {}, multiValueHeaders: event.multiValueHeaders ? {} : undefined, statusCode: res.status, isBase64Encoded, } this.setCookies(event, res, result) res.headers.forEach((value, key) => { result.headers[key] = value if (event.multiValueHeaders && result.multiValueHeaders) { result.multiValueHeaders[key] = [value] } }) return result } setCookies(event: E, res: Response, result: APIGatewayProxyResult) { if (res.headers.has('set-cookie')) { const cookies = res.headers.getSetCookie ? res.headers.getSetCookie() : Array.from(res.headers.entries()) .filter(([k]) => k === 'set-cookie') .map(([, v]) => v) if (Array.isArray(cookies)) { this.setCookiesToResult(event, result, cookies) res.headers.delete('set-cookie') } } } } export class EventV2Processor extends EventProcessor<APIGatewayProxyEventV2> { protected getPath(event: APIGatewayProxyEventV2): string { return event.rawPath } protected getMethod(event: APIGatewayProxyEventV2): string { return event.requestContext.http.method } protected getQueryString(event: APIGatewayProxyEventV2): string { return event.rawQueryString } protected getCookies(event: APIGatewayProxyEventV2, headers: Headers): void { if (Array.isArray(event.cookies)) { headers.set('Cookie', event.cookies.join('; ')) } } protected setCookiesToResult( _: APIGatewayProxyEventV2, result: APIGatewayProxyResult, cookies: string[] ): void { result.cookies = cookies } protected getHeaders(event: APIGatewayProxyEventV2): Headers { const headers = new Headers() this.getCookies(event, headers) if (event.headers) { for (const [k, v] of Object.entries(event.headers)) { if (v) { headers.set(k, v) } } } return headers } } const v2Processor: EventV2Processor = new EventV2Processor() export class EventV1Processor extends EventProcessor<Exclude<LambdaEvent, APIGatewayProxyEventV2>> { protected getPath(event: Exclude<LambdaEvent, APIGatewayProxyEventV2>): string { return event.path } protected getMethod(event: Exclude<LambdaEvent, APIGatewayProxyEventV2>): string { return event.httpMethod } protected getQueryString(event: Exclude<LambdaEvent, APIGatewayProxyEventV2>): string { // In the case of gateway Integration either queryStringParameters or multiValueQueryStringParameters can be present not both if (event.multiValueQueryStringParameters) { return Object.entries(event.multiValueQueryStringParameters || {}) .filter(([, value]) => value) .map(([key, value]) => `${key}=${value.join(`&${key}=`)}`) .join('&') } else { return Object.entries(event.queryStringParameters || {}) .filter(([, value]) => value) .map(([key, value]) => `${key}=${value}`) .join('&') } } protected getCookies( // eslint-disable-next-line @typescript-eslint/no-unused-vars event: Exclude<LambdaEvent, APIGatewayProxyEventV2>, // eslint-disable-next-line @typescript-eslint/no-unused-vars headers: Headers ): void { // nop } protected getHeaders(event: APIGatewayProxyEvent): Headers { const headers = new Headers() this.getCookies(event, headers) if (event.headers) { for (const [k, v] of Object.entries(event.headers)) { if (v) { headers.set(k, v) } } } if (event.multiValueHeaders) { for (const [k, values] of Object.entries(event.multiValueHeaders)) { if (values) { // avoid duplicating already set headers const foundK = headers.get(k) values.forEach((v) => (!foundK || !foundK.includes(v)) && headers.append(k, v)) } } } return headers } protected setCookiesToResult( _: APIGatewayProxyEvent, result: APIGatewayProxyResult, cookies: string[] ): void { result.multiValueHeaders = { 'set-cookie': cookies, } } } const v1Processor: EventV1Processor = new EventV1Processor() export class ALBProcessor extends EventProcessor<ALBProxyEvent> { protected getHeaders(event: ALBProxyEvent): Headers { const headers = new Headers() // if multiValueHeaders is present the ALB will use it instead of the headers field // https://docs.aws.amazon.com/elasticloadbalancing/latest/application/lambda-functions.html#multi-value-headers if (event.multiValueHeaders) { for (const [key, values] of Object.entries(event.multiValueHeaders)) { if (values && Array.isArray(values)) { // https://www.rfc-editor.org/rfc/rfc9110.html#name-common-rules-for-defining-f headers.set(key, values.join('; ')) } } } else { for (const [key, value] of Object.entries(event.headers ?? {})) { if (value) { headers.set(key, value) } } } return headers } protected getPath(event: ALBProxyEvent): string { return event.path } protected getMethod(event: ALBProxyEvent): string { return event.httpMethod } protected getQueryString(event: ALBProxyEvent): string { // In the case of ALB Integration either queryStringParameters or multiValueQueryStringParameters can be present not both /* In other cases like when using the serverless framework, the event object does contain both queryStringParameters and multiValueQueryStringParameters: Below is an example event object for this URL: /payment/b8c55e69?select=amount&select=currency { ... queryStringParameters: { select: 'currency' }, multiValueQueryStringParameters: { select: [ 'amount', 'currency' ] }, } The expected results is for select to be an array with two items. However the pre-fix code is only returning one item ('currency') in the array. A simple fix would be to invert the if statement and check the multiValueQueryStringParameters first. */ if (event.multiValueQueryStringParameters) { return Object.entries(event.multiValueQueryStringParameters || {}) .filter(([, value]) => value) .map(([key, value]) => `${key}=${value.join(`&${key}=`)}`) .join('&') } else { return Object.entries(event.queryStringParameters || {}) .filter(([, value]) => value) .map(([key, value]) => `${key}=${value}`) .join('&') } } protected getCookies(event: ALBProxyEvent, headers: Headers): void { let cookie if (event.multiValueHeaders) { cookie = event.multiValueHeaders['cookie']?.join('; ') } else { cookie = event.headers ? event.headers['cookie'] : undefined } if (cookie) { headers.append('Cookie', cookie) } } protected setCookiesToResult( event: ALBProxyEvent, result: APIGatewayProxyResult, cookies: string[] ): void { // when multi value headers is enabled if (event.multiValueHeaders && result.multiValueHeaders) { result.multiValueHeaders['set-cookie'] = cookies } else { // otherwise serialize the set-cookie result.headers['set-cookie'] = cookies.join(', ') } } } const albProcessor: ALBProcessor = new ALBProcessor() export const getProcessor = (event: LambdaEvent): EventProcessor<LambdaEvent> => { if (isProxyEventALB(event)) { return albProcessor } if (isProxyEventV2(event)) { return v2Processor } return v1Processor } const isProxyEventALB = (event: LambdaEvent): event is ALBProxyEvent => { if (event.requestContext) { return Object.hasOwn(event.requestContext, 'elb') } return false } const isProxyEventV2 = (event: LambdaEvent): event is APIGatewayProxyEventV2 => { return Object.hasOwn(event, 'rawPath') } export const isContentTypeBinary = (contentType: string) => { return !/^(text\/(plain|html|css|javascript|csv).*|application\/(.*json|.*xml).*|image\/svg\+xml.*)$/.test( contentType ) } export const isContentEncodingBinary = (contentEncoding: string | null) => { if (contentEncoding === null) { return false } return /^(gzip|deflate|compress|br)/.test(contentEncoding) }