From b36d9c0722fd3439d1126717a31c1588b9a496db Mon Sep 17 00:00:00 2001 From: exoego Date: Thu, 11 Apr 2024 15:39:06 +0900 Subject: [PATCH 1/8] Headers can be undefined on ALB --- src/adapter/aws-lambda/handler.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/adapter/aws-lambda/handler.ts b/src/adapter/aws-lambda/handler.ts index a61b1f935..4451e14bd 100644 --- a/src/adapter/aws-lambda/handler.ts +++ b/src/adapter/aws-lambda/handler.ts @@ -22,6 +22,7 @@ export interface APIGatewayProxyEventV2 { version: string routeKey: string headers: Record + multiValueHeaders: undefined cookies?: string[] rawPath: string rawQueryString: string @@ -63,7 +64,8 @@ export interface APIGatewayProxyEvent { // When calling Lambda through an Application Load Balancer export interface ALBProxyEvent { httpMethod: string - headers: Record + headers?: Record + multiValueHeaders?: Record path: string body: string | null isBase64Encoded: boolean @@ -198,14 +200,15 @@ const createRequest = (event: LambdaEvent) => { const domainName = event.requestContext && 'domainName' in event.requestContext ? event.requestContext.domainName - : event.headers['host'] + : event.headers?.['host'] + ?? event.multiValueHeaders?.['host']?.[0] const path = isProxyEventV2(event) ? event.rawPath : event.path const urlPath = `https://${domainName}${path}` const url = queryString ? `${urlPath}?${queryString}` : urlPath const headers = new Headers() getCookies(event, headers) - for (const [k, v] of Object.entries(event.headers)) { + for (const [k, v] of Object.entries(event.headers || {})) { if (v) { headers.set(k, v) } From 0e5ec2ccfda104116b65bb907a474bc81b41a11c Mon Sep 17 00:00:00 2001 From: exoego Date: Thu, 11 Apr 2024 15:39:28 +0900 Subject: [PATCH 2/8] Delegate headers from multiValueHeaders --- src/adapter/aws-lambda/handler.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/adapter/aws-lambda/handler.ts b/src/adapter/aws-lambda/handler.ts index 4451e14bd..a74f04539 100644 --- a/src/adapter/aws-lambda/handler.ts +++ b/src/adapter/aws-lambda/handler.ts @@ -213,6 +213,11 @@ const createRequest = (event: LambdaEvent) => { headers.set(k, v) } } + for (const [k, values] of Object.entries(event.multiValueHeaders || {})) { + if (values) { + values.forEach((v) => headers.append(k, v)) + } + } const method = isProxyEventV2(event) ? event.requestContext.http.method : event.httpMethod const requestInit: RequestInit = { From 0ec2c095cc4a0a56779575d2e00f3f4d04f19dc6 Mon Sep 17 00:00:00 2001 From: exoego Date: Thu, 11 Apr 2024 15:51:24 +0900 Subject: [PATCH 3/8] Add test --- src/adapter/aws-lambda/handler.test.ts | 65 +++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/src/adapter/aws-lambda/handler.test.ts b/src/adapter/aws-lambda/handler.test.ts index f35dcafe9..f3e1bf8e1 100644 --- a/src/adapter/aws-lambda/handler.test.ts +++ b/src/adapter/aws-lambda/handler.test.ts @@ -1,4 +1,6 @@ -import { isContentTypeBinary, isContentEncodingBinary } from './handler' +import { Hono } from '../../hono' +import { isContentTypeBinary, isContentEncodingBinary, handle } from './handler' +import type { ALBProxyEvent } from './handler' describe('isContentTypeBinary', () => { it('Should determine whether it is binary', () => { @@ -27,3 +29,64 @@ describe('isContentEncodingBinary', () => { expect(isContentEncodingBinary('unknown')).toBe(false) }) }) + +describe('handle', () => { + const dummyLambdaContext = { + awsRequestId: '', + callbackWaitsForEmptyEventLoop: false, + functionName: '', + functionVersion: '', + invokedFunctionArn: '', + logGroupName: '', + logStreamName: '', + memoryLimitInMB: '', + getRemainingTimeInMillis(): number { + return 0 + } + } + + describe('ALB', () => { + const app = new Hono().post('/', (c) => { + return c.json({ message: 'ok' }) + }) + const handler = handle(app) + + it('Should accept single value headers', async () => { + const event: ALBProxyEvent = { + body: '{}', + httpMethod: 'POST', + isBase64Encoded: false, + path: '/', + headers: { + host: 'localhost', + }, + requestContext: { + elb: { + targetGroupArn: '', + } + } + } + const response = await handler(event, dummyLambdaContext) + expect(response?.['statusCode']).toEqual(200) + }) + + it('Should accept multi value headers', async () => { + const event: ALBProxyEvent = { + body: '{}', + httpMethod: 'POST', + isBase64Encoded: false, + path: '/', + multiValueHeaders: { + host: ['localhost'], + }, + requestContext: { + elb: { + targetGroupArn: '', + } + } + } + const response = await handler(event, dummyLambdaContext) + expect(response?.['statusCode']).toEqual(200) + }) + }) +}) \ No newline at end of file From 485d98c175cf077c3c8d982ca9db62e71ebabfeb Mon Sep 17 00:00:00 2001 From: exoego Date: Thu, 11 Apr 2024 16:13:38 +0900 Subject: [PATCH 4/8] Assert header values --- src/adapter/aws-lambda/handler.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/adapter/aws-lambda/handler.test.ts b/src/adapter/aws-lambda/handler.test.ts index f3e1bf8e1..eed7d1169 100644 --- a/src/adapter/aws-lambda/handler.test.ts +++ b/src/adapter/aws-lambda/handler.test.ts @@ -47,7 +47,10 @@ describe('handle', () => { describe('ALB', () => { const app = new Hono().post('/', (c) => { - return c.json({ message: 'ok' }) + if (c.req.header('foo')?.includes('bar')) { + return c.json({ message: 'ok' }) + } + return c.json({ message: 'fail' }, 400) }) const handler = handle(app) @@ -59,6 +62,7 @@ describe('handle', () => { path: '/', headers: { host: 'localhost', + foo: 'bar', }, requestContext: { elb: { @@ -78,6 +82,7 @@ describe('handle', () => { path: '/', multiValueHeaders: { host: ['localhost'], + foo: ['bar', 'buz'], }, requestContext: { elb: { From 57967de8e14f647b70ab3578bc5deabea09c67e1 Mon Sep 17 00:00:00 2001 From: exoego Date: Thu, 11 Apr 2024 16:14:14 +0900 Subject: [PATCH 5/8] format --- src/adapter/aws-lambda/handler.test.ts | 14 +++++++------- src/adapter/aws-lambda/handler.ts | 3 +-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/adapter/aws-lambda/handler.test.ts b/src/adapter/aws-lambda/handler.test.ts index eed7d1169..90b77a914 100644 --- a/src/adapter/aws-lambda/handler.test.ts +++ b/src/adapter/aws-lambda/handler.test.ts @@ -1,5 +1,5 @@ import { Hono } from '../../hono' -import { isContentTypeBinary, isContentEncodingBinary, handle } from './handler' +import { isContentTypeBinary, isContentEncodingBinary, handle } from './handler' import type { ALBProxyEvent } from './handler' describe('isContentTypeBinary', () => { @@ -42,7 +42,7 @@ describe('handle', () => { memoryLimitInMB: '', getRemainingTimeInMillis(): number { return 0 - } + }, } describe('ALB', () => { @@ -67,8 +67,8 @@ describe('handle', () => { requestContext: { elb: { targetGroupArn: '', - } - } + }, + }, } const response = await handler(event, dummyLambdaContext) expect(response?.['statusCode']).toEqual(200) @@ -87,11 +87,11 @@ describe('handle', () => { requestContext: { elb: { targetGroupArn: '', - } - } + }, + }, } const response = await handler(event, dummyLambdaContext) expect(response?.['statusCode']).toEqual(200) }) }) -}) \ No newline at end of file +}) diff --git a/src/adapter/aws-lambda/handler.ts b/src/adapter/aws-lambda/handler.ts index a74f04539..0c8b94d12 100644 --- a/src/adapter/aws-lambda/handler.ts +++ b/src/adapter/aws-lambda/handler.ts @@ -200,8 +200,7 @@ const createRequest = (event: LambdaEvent) => { const domainName = event.requestContext && 'domainName' in event.requestContext ? event.requestContext.domainName - : event.headers?.['host'] - ?? event.multiValueHeaders?.['host']?.[0] + : event.headers?.['host'] ?? event.multiValueHeaders?.['host']?.[0] const path = isProxyEventV2(event) ? event.rawPath : event.path const urlPath = `https://${domainName}${path}` const url = queryString ? `${urlPath}?${queryString}` : urlPath From 72336fd1f7332c24dbad33fa65ba6b6b85f12d65 Mon Sep 17 00:00:00 2001 From: exoego Date: Thu, 11 Apr 2024 20:50:51 +0900 Subject: [PATCH 6/8] Make multiValueHeaders optional so it can be omitted --- src/adapter/aws-lambda/handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/adapter/aws-lambda/handler.ts b/src/adapter/aws-lambda/handler.ts index 0c8b94d12..8e636862a 100644 --- a/src/adapter/aws-lambda/handler.ts +++ b/src/adapter/aws-lambda/handler.ts @@ -22,7 +22,7 @@ export interface APIGatewayProxyEventV2 { version: string routeKey: string headers: Record - multiValueHeaders: undefined + multiValueHeaders?: undefined cookies?: string[] rawPath: string rawQueryString: string From 389da2c2a8bfb00ee2366a24e4a18a3e00b6ced8 Mon Sep 17 00:00:00 2001 From: exoego Date: Thu, 11 Apr 2024 21:06:00 +0900 Subject: [PATCH 7/8] Avoid Object.entries when undefined --- src/adapter/aws-lambda/handler.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/adapter/aws-lambda/handler.ts b/src/adapter/aws-lambda/handler.ts index 8e636862a..3b169a901 100644 --- a/src/adapter/aws-lambda/handler.ts +++ b/src/adapter/aws-lambda/handler.ts @@ -207,14 +207,17 @@ const createRequest = (event: LambdaEvent) => { const headers = new Headers() getCookies(event, headers) - for (const [k, v] of Object.entries(event.headers || {})) { - if (v) { - headers.set(k, v) + if (event.headers) { + for (const [k, v] of Object.entries(event.headers)) { + if (v) { + headers.set(k, v) + } } - } - for (const [k, values] of Object.entries(event.multiValueHeaders || {})) { - if (values) { - values.forEach((v) => headers.append(k, v)) + } else if (event.multiValueHeaders) { + for (const [k, values] of Object.entries(event.multiValueHeaders)) { + if (values) { + values.forEach((v) => headers.append(k, v)) + } } } From d51f022def6dc6855de3a816ff3cfac1cd9b2506 Mon Sep 17 00:00:00 2001 From: exoego Date: Fri, 12 Apr 2024 06:34:13 +0900 Subject: [PATCH 8/8] Write tests in runtime_tests --- runtime_tests/lambda/index.test.ts | 101 +++++++++++++++++++++++++ src/adapter/aws-lambda/handler.test.ts | 70 +---------------- src/adapter/aws-lambda/handler.ts | 3 +- 3 files changed, 104 insertions(+), 70 deletions(-) diff --git a/runtime_tests/lambda/index.test.ts b/runtime_tests/lambda/index.test.ts index fdc2c0c1d..418c92ab8 100644 --- a/runtime_tests/lambda/index.test.ts +++ b/runtime_tests/lambda/index.test.ts @@ -139,6 +139,13 @@ describe('AWS Lambda Adapter for Hono', () => { return c.text('Valid Cookies') }) + app.post('/headers', (c) => { + if (c.req.header('foo')?.includes('bar')) { + return c.json({ message: 'ok' }) + } + return c.json({ message: 'fail' }, 400) + }) + const handler = handle(app) const testApiGatewayRequestContext = { @@ -491,6 +498,100 @@ describe('AWS Lambda Adapter for Hono', () => { ]) }) + describe('headers', () => { + describe('single-value headers', () => { + it('Should extract single-value headers and return 200 (ALBProxyEvent)', async () => { + const event = { + body: '{}', + httpMethod: 'POST', + isBase64Encoded: false, + path: '/headers', + headers: { + host: 'localhost', + foo: 'bar', + }, + requestContext: testALBRequestContext, + } + const apiGatewayResponseV2 = await handler(event) + expect(apiGatewayResponseV2.statusCode).toBe(200) + }) + + it('Should extract single-value headers and return 200 (APIGatewayProxyEvent)', async () => { + const apigatewayProxyEvent = { + version: '1.0', + resource: '/headers', + httpMethod: 'POST', + headers: { + host: 'localhost', + foo: 'bar', + }, + path: '/headers', + body: null, + isBase64Encoded: false, + requestContext: testApiGatewayRequestContext, + } + const apiGatewayResponseV2 = await handler(apigatewayProxyEvent) + expect(apiGatewayResponseV2.statusCode).toBe(200) + }) + + it('Should extract single-value headers and return 200 (APIGatewayProxyEventV2)', async () => { + const apigatewayProxyV2Event = { + version: '2.0', + routeKey: '$default', + headers: { + host: 'localhost', + foo: 'bar', + }, + rawPath: '/headers', + rawQueryString: '', + requestContext: testApiGatewayRequestContextV2, + resource: '/headers', + body: null, + isBase64Encoded: false, + } + const apiGatewayResponseV2 = await handler(apigatewayProxyV2Event) + expect(apiGatewayResponseV2.statusCode).toBe(200) + }) + }) + + describe('multi-value headers', () => { + it('Should extract multi-value headers and return 200 (ALBProxyEvent)', async () => { + const event = { + body: '{}', + httpMethod: 'POST', + isBase64Encoded: false, + path: '/headers', + multiValueHeaders: { + host: ['localhost'], + foo: ['bar'], + }, + requestContext: testALBRequestContext, + } + const apiGatewayResponseV2 = await handler(event) + expect(apiGatewayResponseV2.statusCode).toBe(200) + }) + + it('Should extract multi-value headers and return 200 (APIGatewayProxyEvent)', async () => { + const apigatewayProxyEvent = { + version: '1.0', + resource: '/headers', + httpMethod: 'POST', + headers: {}, + multiValueHeaders: { + host: ['localhost'], + foo: ['bar'], + }, + path: '/headers', + body: null, + isBase64Encoded: false, + requestContext: testApiGatewayRequestContext, + } + const apiGatewayResponseV2 = await handler(apigatewayProxyEvent) + expect(apiGatewayResponseV2.statusCode).toBe(200) + }) + }) + }) + it('Should handle a POST request and return a 200 response if cookies match (APIGatewayProxyEvent V1 and V2)', async () => { const apiGatewayEvent = { version: '1.0', diff --git a/src/adapter/aws-lambda/handler.test.ts b/src/adapter/aws-lambda/handler.test.ts index 90b77a914..f35dcafe9 100644 --- a/src/adapter/aws-lambda/handler.test.ts +++ b/src/adapter/aws-lambda/handler.test.ts @@ -1,6 +1,4 @@ -import { Hono } from '../../hono' -import { isContentTypeBinary, isContentEncodingBinary, handle } from './handler' -import type { ALBProxyEvent } from './handler' +import { isContentTypeBinary, isContentEncodingBinary } from './handler' describe('isContentTypeBinary', () => { it('Should determine whether it is binary', () => { @@ -29,69 +27,3 @@ describe('isContentEncodingBinary', () => { expect(isContentEncodingBinary('unknown')).toBe(false) }) }) - -describe('handle', () => { - const dummyLambdaContext = { - awsRequestId: '', - callbackWaitsForEmptyEventLoop: false, - functionName: '', - functionVersion: '', - invokedFunctionArn: '', - logGroupName: '', - logStreamName: '', - memoryLimitInMB: '', - getRemainingTimeInMillis(): number { - return 0 - }, - } - - describe('ALB', () => { - const app = new Hono().post('/', (c) => { - if (c.req.header('foo')?.includes('bar')) { - return c.json({ message: 'ok' }) - } - return c.json({ message: 'fail' }, 400) - }) - const handler = handle(app) - - it('Should accept single value headers', async () => { - const event: ALBProxyEvent = { - body: '{}', - httpMethod: 'POST', - isBase64Encoded: false, - path: '/', - headers: { - host: 'localhost', - foo: 'bar', - }, - requestContext: { - elb: { - targetGroupArn: '', - }, - }, - } - const response = await handler(event, dummyLambdaContext) - expect(response?.['statusCode']).toEqual(200) - }) - - it('Should accept multi value headers', async () => { - const event: ALBProxyEvent = { - body: '{}', - httpMethod: 'POST', - isBase64Encoded: false, - path: '/', - multiValueHeaders: { - host: ['localhost'], - foo: ['bar', 'buz'], - }, - requestContext: { - elb: { - targetGroupArn: '', - }, - }, - } - const response = await handler(event, dummyLambdaContext) - expect(response?.['statusCode']).toEqual(200) - }) - }) -}) diff --git a/src/adapter/aws-lambda/handler.ts b/src/adapter/aws-lambda/handler.ts index 3b169a901..c3cb275b4 100644 --- a/src/adapter/aws-lambda/handler.ts +++ b/src/adapter/aws-lambda/handler.ts @@ -213,7 +213,8 @@ const createRequest = (event: LambdaEvent) => { headers.set(k, v) } } - } else if (event.multiValueHeaders) { + } + if (event.multiValueHeaders) { for (const [k, values] of Object.entries(event.multiValueHeaders)) { if (values) { values.forEach((v) => headers.append(k, v))