Skip to content

Commit 0e4dfe5

Browse files
authored
fix(adapter/aws-lambda): APIGWProxyResult should contain either of headers or multiValueHeaders, not both (#3883)
* fix(adapter/aws-lambda): API GW Proxy Result should contain either of headers or multiValueHeaders, not both, to avoid confusing header merge mechanism * fix(adapter/aws-lambda): Infer response type from event
1 parent 035c2d7 commit 0e4dfe5

File tree

2 files changed

+63
-42
lines changed

2 files changed

+63
-42
lines changed

runtime-tests/lambda/index.test.ts

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ describe('AWS Lambda Adapter for Hono', () => {
249249
expect(response.statusCode).toBe(200)
250250
expect(response.body).toBe('Hello Lambda!')
251251
expect(response.headers['content-type']).toMatch(/^text\/plain/)
252+
expect(response.multiValueHeaders).toBeUndefined()
252253
expect(response.isBase64Encoded).toBe(false)
253254
})
254255

@@ -268,6 +269,7 @@ describe('AWS Lambda Adapter for Hono', () => {
268269
expect(response.statusCode).toBe(200)
269270
expect(response.body).toBe('RmFrZSBJbWFnZQ==')
270271
expect(response.headers['content-type']).toMatch(/^image\/png/)
272+
expect(response.multiValueHeaders).toBeUndefined()
271273
expect(response.isBase64Encoded).toBe(true)
272274
})
273275

@@ -289,6 +291,7 @@ describe('AWS Lambda Adapter for Hono', () => {
289291
expect(response.statusCode).toBe(200)
290292
expect(response.body).toBe('Hello Lambda!')
291293
expect(response.headers['content-type']).toMatch(/^text\/plain/)
294+
expect(response.multiValueHeaders).toBeUndefined()
292295
expect(response.isBase64Encoded).toBe(false)
293296
})
294297

@@ -309,6 +312,7 @@ describe('AWS Lambda Adapter for Hono', () => {
309312
expect(response.statusCode).toBe(200)
310313
expect(response.body).toBe('Hello Lambda!')
311314
expect(response.headers['content-type']).toMatch(/^text\/plain/)
315+
expect(response.multiValueHeaders).toBeUndefined()
312316
expect(response.isBase64Encoded).toBe(false)
313317
})
314318

@@ -540,6 +544,7 @@ describe('AWS Lambda Adapter for Hono', () => {
540544
'content-type': 'application/json',
541545
})
542546
)
547+
expect(albResponse.multiValueHeaders).toBeUndefined()
543548
})
544549

545550
it('Should extract single-value headers and return 200 (APIGatewayProxyEvent)', async () => {
@@ -687,6 +692,7 @@ describe('AWS Lambda Adapter for Hono', () => {
687692
expect(albResponse.statusCode).toBe(200)
688693
expect(albResponse.body).toBe('Valid Cookies')
689694
expect(albResponse.headers['content-type']).toMatch(/^text\/plain/)
695+
expect(albResponse.multiValueHeaders).toBeUndefined()
690696
expect(albResponse.isBase64Encoded).toBe(false)
691697
})
692698

@@ -709,7 +715,10 @@ describe('AWS Lambda Adapter for Hono', () => {
709715

710716
expect(albResponse.statusCode).toBe(200)
711717
expect(albResponse.body).toBe('Valid Cookies')
712-
expect(albResponse.headers['content-type']).toMatch(/^text\/plain/)
718+
expect(albResponse.headers).toBeUndefined()
719+
expect(albResponse.multiValueHeaders['content-type']).toEqual([
720+
expect.stringMatching(/^text\/plain/),
721+
])
713722
expect(albResponse.isBase64Encoded).toBe(false)
714723
})
715724

@@ -759,9 +768,8 @@ describe('AWS Lambda Adapter for Hono', () => {
759768

760769
expect(albResponse.statusCode).toBe(200)
761770
expect(albResponse.body).toBe('Cookies Set')
762-
expect(albResponse.headers['content-type']).toMatch(/^text\/plain/)
763-
expect(albResponse.multiValueHeaders).toBeDefined()
764-
expect(albResponse.multiValueHeaders && albResponse.multiValueHeaders['set-cookie']).toEqual(
771+
expect(albResponse.headers).toBeUndefined()
772+
expect(albResponse.multiValueHeaders['set-cookie']).toEqual(
765773
expect.arrayContaining([testCookie1.serialized, testCookie2.serialized])
766774
)
767775
expect(albResponse.isBase64Encoded).toBe(false)
@@ -794,6 +802,7 @@ describe('AWS Lambda Adapter for Hono', () => {
794802
})
795803
)
796804
expect(albResponse.headers['content-type']).toMatch(/^application\/json/)
805+
expect(albResponse.multiValueHeaders).toBeUndefined()
797806
expect(albResponse.isBase64Encoded).toBe(false)
798807
})
799808

@@ -823,7 +832,10 @@ describe('AWS Lambda Adapter for Hono', () => {
823832
key2: 'value2',
824833
})
825834
)
826-
expect(albResponse.headers['content-type']).toMatch(/^application\/json/)
835+
expect(albResponse.headers).toBeUndefined()
836+
expect(albResponse.multiValueHeaders['content-type']).toEqual([
837+
expect.stringMatching(/^application\/json/),
838+
])
827839
expect(albResponse.isBase64Encoded).toBe(false)
828840
})
829841

@@ -853,7 +865,10 @@ describe('AWS Lambda Adapter for Hono', () => {
853865
key2: ['value2', 'otherValue2'],
854866
})
855867
)
856-
expect(albResponse.headers['content-type']).toMatch(/^application\/json/)
868+
expect(albResponse.headers).toBeUndefined()
869+
expect(albResponse.multiValueHeaders['content-type']).toEqual([
870+
expect.stringMatching(/^application\/json/),
871+
])
857872
expect(albResponse.isBase64Encoded).toBe(false)
858873
})
859874
})

src/adapter/aws-lambda/handler.ts

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -70,17 +70,22 @@ export interface ALBProxyEvent {
7070
requestContext: ALBRequestContext
7171
}
7272

73-
export interface APIGatewayProxyResult {
73+
type WithHeaders = {
74+
headers: Record<string, string>
75+
multiValueHeaders?: undefined
76+
}
77+
type WithMultiValueHeaders = {
78+
headers?: undefined
79+
multiValueHeaders: Record<string, string[]>
80+
}
81+
82+
export type APIGatewayProxyResult = {
7483
statusCode: number
7584
statusDescription?: string
7685
body: string
77-
headers: Record<string, string>
7886
cookies?: string[]
79-
multiValueHeaders?: {
80-
[headerKey: string]: string[]
81-
}
8287
isBase64Encoded: boolean
83-
}
88+
} & (WithHeaders | WithMultiValueHeaders)
8489

8590
const getRequestContext = (
8691
event: LambdaEvent
@@ -162,7 +167,16 @@ export const streamHandle = <
162167
*/
163168
export const handle = <E extends Env = Env, S extends Schema = {}, BasePath extends string = '/'>(
164169
app: Hono<E, S, BasePath>
165-
): ((event: LambdaEvent, lambdaContext?: LambdaContext) => Promise<APIGatewayProxyResult>) => {
170+
): (<L extends LambdaEvent>(
171+
event: L,
172+
lambdaContext?: LambdaContext
173+
) => Promise<
174+
APIGatewayProxyResult &
175+
(L extends { multiValueHeaders: Record<string, string[]> }
176+
? WithMultiValueHeaders
177+
: WithHeaders)
178+
>) => {
179+
// @ts-expect-error FIXME: Fix return typing
166180
return async (event, lambdaContext?) => {
167181
const processor = getProcessor(event)
168182

@@ -190,11 +204,7 @@ export abstract class EventProcessor<E extends LambdaEvent> {
190204

191205
protected abstract getCookies(event: E, headers: Headers): void
192206

193-
protected abstract setCookiesToResult(
194-
event: E,
195-
result: APIGatewayProxyResult,
196-
cookies: string[]
197-
): void
207+
protected abstract setCookiesToResult(result: APIGatewayProxyResult, cookies: string[]): void
198208

199209
createRequest(event: E): Request {
200210
const queryString = this.getQueryString(event)
@@ -234,19 +244,27 @@ export abstract class EventProcessor<E extends LambdaEvent> {
234244

235245
const result: APIGatewayProxyResult = {
236246
body: body,
237-
headers: {},
238-
multiValueHeaders: event.multiValueHeaders ? {} : undefined,
239247
statusCode: res.status,
240248
isBase64Encoded,
249+
...(event.multiValueHeaders
250+
? {
251+
multiValueHeaders: {},
252+
}
253+
: {
254+
headers: {},
255+
}),
241256
}
242257

243258
this.setCookies(event, res, result)
244-
res.headers.forEach((value, key) => {
245-
result.headers[key] = value
246-
if (event.multiValueHeaders && result.multiValueHeaders) {
259+
if (result.multiValueHeaders) {
260+
res.headers.forEach((value, key) => {
247261
result.multiValueHeaders[key] = [value]
248-
}
249-
})
262+
})
263+
} else {
264+
res.headers.forEach((value, key) => {
265+
result.headers[key] = value
266+
})
267+
}
250268

251269
return result
252270
}
@@ -260,7 +278,7 @@ export abstract class EventProcessor<E extends LambdaEvent> {
260278
.map(([, v]) => v)
261279

262280
if (Array.isArray(cookies)) {
263-
this.setCookiesToResult(event, result, cookies)
281+
this.setCookiesToResult(result, cookies)
264282
res.headers.delete('set-cookie')
265283
}
266284
}
@@ -286,11 +304,7 @@ export class EventV2Processor extends EventProcessor<APIGatewayProxyEventV2> {
286304
}
287305
}
288306

289-
protected setCookiesToResult(
290-
_: APIGatewayProxyEventV2,
291-
result: APIGatewayProxyResult,
292-
cookies: string[]
293-
): void {
307+
protected setCookiesToResult(result: APIGatewayProxyResult, cookies: string[]): void {
294308
result.cookies = cookies
295309
}
296310

@@ -365,11 +379,7 @@ export class EventV1Processor extends EventProcessor<Exclude<LambdaEvent, APIGat
365379
return headers
366380
}
367381

368-
protected setCookiesToResult(
369-
_: APIGatewayProxyEvent,
370-
result: APIGatewayProxyResult,
371-
cookies: string[]
372-
): void {
382+
protected setCookiesToResult(result: APIGatewayProxyResult, cookies: string[]): void {
373383
result.multiValueHeaders = {
374384
'set-cookie': cookies,
375385
}
@@ -446,13 +456,9 @@ export class ALBProcessor extends EventProcessor<ALBProxyEvent> {
446456
}
447457
}
448458

449-
protected setCookiesToResult(
450-
event: ALBProxyEvent,
451-
result: APIGatewayProxyResult,
452-
cookies: string[]
453-
): void {
459+
protected setCookiesToResult(result: APIGatewayProxyResult, cookies: string[]): void {
454460
// when multi value headers is enabled
455-
if (event.multiValueHeaders && result.multiValueHeaders) {
461+
if (result.multiValueHeaders) {
456462
result.multiValueHeaders['set-cookie'] = cookies
457463
} else {
458464
// otherwise serialize the set-cookie

0 commit comments

Comments
 (0)