1
+ import { Readable } from 'node:stream' ;
2
+ import type streamWeb from 'node:stream/web' ;
3
+ import { isString } from '@aws-lambda-powertools/commons/typeutils' ;
1
4
import type { APIGatewayProxyEvent , APIGatewayProxyResult } from 'aws-lambda' ;
2
5
import type {
3
6
CompressionOptions ,
7
+ ExtendedAPIGatewayProxyResultBody ,
4
8
HandlerResponse ,
5
9
HttpStatusCode ,
6
10
} from '../types/rest.js' ;
7
11
import { COMPRESSION_ENCODING_TYPES , HttpStatusCodes } from './constants.js' ;
8
- import { isAPIGatewayProxyResult } from './utils.js' ;
12
+ import {
13
+ isExtendedAPIGatewayProxyResult ,
14
+ isNodeReadableStream ,
15
+ isWebReadableStream ,
16
+ } from './utils.js' ;
9
17
10
18
/**
11
19
* Creates a request body from API Gateway event body, handling base64 decoding if needed.
@@ -72,18 +80,17 @@ const proxyEventToWebRequest = (event: APIGatewayProxyEvent): Request => {
72
80
} ;
73
81
74
82
/**
75
- * Converts a Web API Response object to an API Gateway proxy result.
83
+ * Converts Web API Headers to API Gateway v1 headers format.
84
+ * Splits multi-value headers by comma and organizes them into separate objects.
76
85
*
77
- * @param response - The Web API Response object
78
- * @returns An API Gateway proxy result
86
+ * @param webHeaders - The Web API Headers object
87
+ * @returns Object containing headers and multiValueHeaders
79
88
*/
80
- const webResponseToProxyResult = async (
81
- response : Response
82
- ) : Promise < APIGatewayProxyResult > => {
89
+ const webHeadersToApiGatewayV1Headers = ( webHeaders : Headers ) => {
83
90
const headers : Record < string , string > = { } ;
84
91
const multiValueHeaders : Record < string , Array < string > > = { } ;
85
92
86
- for ( const [ key , value ] of response . headers . entries ( ) ) {
93
+ for ( const [ key , value ] of webHeaders . entries ( ) ) {
87
94
const values = value . split ( ',' ) . map ( ( v ) => v . trimStart ( ) ) ;
88
95
if ( values . length > 1 ) {
89
96
multiValueHeaders [ key ] = values ;
@@ -92,6 +99,25 @@ const webResponseToProxyResult = async (
92
99
}
93
100
}
94
101
102
+ return {
103
+ headers,
104
+ multiValueHeaders,
105
+ } ;
106
+ } ;
107
+
108
+ /**
109
+ * Converts a Web API Response object to an API Gateway proxy result.
110
+ *
111
+ * @param response - The Web API Response object
112
+ * @returns An API Gateway proxy result
113
+ */
114
+ const webResponseToProxyResult = async (
115
+ response : Response
116
+ ) : Promise < APIGatewayProxyResult > => {
117
+ const { headers, multiValueHeaders } = webHeadersToApiGatewayV1Headers (
118
+ response . headers
119
+ ) ;
120
+
95
121
// Check if response contains compressed/binary content
96
122
const contentEncoding = response . headers . get (
97
123
'content-encoding'
@@ -135,12 +161,14 @@ const webResponseToProxyResult = async (
135
161
*
136
162
* @param response - The handler response (APIGatewayProxyResult, Response, or plain object)
137
163
* @param resHeaders - Optional headers to be included in the response
164
+ * @returns A Web API Response object
138
165
*/
139
166
const handlerResultToWebResponse = (
140
167
response : HandlerResponse ,
141
168
resHeaders ?: Headers
142
169
) : Response => {
143
170
if ( response instanceof Response ) {
171
+ if ( resHeaders === undefined ) return response ;
144
172
const headers = new Headers ( resHeaders ) ;
145
173
for ( const [ key , value ] of response . headers . entries ( ) ) {
146
174
headers . set ( key , value ) ;
@@ -154,7 +182,7 @@ const handlerResultToWebResponse = (
154
182
const headers = new Headers ( resHeaders ) ;
155
183
headers . set ( 'Content-Type' , 'application/json' ) ;
156
184
157
- if ( isAPIGatewayProxyResult ( response ) ) {
185
+ if ( isExtendedAPIGatewayProxyResult ( response ) ) {
158
186
for ( const [ key , value ] of Object . entries ( response . headers ?? { } ) ) {
159
187
if ( value != null ) {
160
188
headers . set ( key , String ( value ) ) ;
@@ -169,7 +197,12 @@ const handlerResultToWebResponse = (
169
197
}
170
198
}
171
199
172
- return new Response ( response . body , {
200
+ const body =
201
+ response . body instanceof Readable
202
+ ? ( Readable . toWeb ( response . body ) as ReadableStream )
203
+ : response . body ;
204
+
205
+ return new Response ( body , {
173
206
status : response . statusCode ,
174
207
headers,
175
208
} ) ;
@@ -189,8 +222,24 @@ const handlerResultToProxyResult = async (
189
222
response : HandlerResponse ,
190
223
statusCode : HttpStatusCode = HttpStatusCodes . OK
191
224
) : Promise < APIGatewayProxyResult > => {
192
- if ( isAPIGatewayProxyResult ( response ) ) {
193
- return response ;
225
+ if ( isExtendedAPIGatewayProxyResult ( response ) ) {
226
+ if ( isString ( response . body ) ) {
227
+ return {
228
+ ...response ,
229
+ body : response . body ,
230
+ } ;
231
+ }
232
+ if (
233
+ isNodeReadableStream ( response . body ) ||
234
+ isWebReadableStream ( response . body )
235
+ ) {
236
+ const nodeStream = bodyToNodeStream ( response . body ) ;
237
+ return {
238
+ ...response ,
239
+ isBase64Encoded : true ,
240
+ body : await nodeStreamToBase64 ( nodeStream ) ,
241
+ } ;
242
+ }
194
243
}
195
244
if ( response instanceof Response ) {
196
245
return await webResponseToProxyResult ( response ) ;
@@ -203,9 +252,43 @@ const handlerResultToProxyResult = async (
203
252
} ;
204
253
} ;
205
254
255
+ /**
256
+ * Converts various body types to a Node.js Readable stream.
257
+ * Handles Node.js streams, web streams, and string bodies.
258
+ *
259
+ * @param body - The body to convert (Readable, ReadableStream, or string)
260
+ * @returns A Node.js Readable stream
261
+ */
262
+ const bodyToNodeStream = ( body : ExtendedAPIGatewayProxyResultBody ) => {
263
+ if ( isNodeReadableStream ( body ) ) {
264
+ return body ;
265
+ }
266
+ if ( isWebReadableStream ( body ) ) {
267
+ return Readable . fromWeb ( body as streamWeb . ReadableStream ) ;
268
+ }
269
+ return Readable . from ( Buffer . from ( body as string ) ) ;
270
+ } ;
271
+
272
+ /**
273
+ * Converts a Node.js Readable stream to a base64 encoded string.
274
+ * Handles both Buffer and string chunks by converting all to Buffers.
275
+ *
276
+ * @param stream - The Node.js Readable stream to convert
277
+ * @returns A Promise that resolves to a base64 encoded string
278
+ */
279
+ async function nodeStreamToBase64 ( stream : Readable ) {
280
+ const chunks : Buffer [ ] = [ ] ;
281
+ for await ( const chunk of stream ) {
282
+ chunks . push ( Buffer . isBuffer ( chunk ) ? chunk : Buffer . from ( chunk ) ) ;
283
+ }
284
+ return Buffer . concat ( chunks ) . toString ( 'base64' ) ;
285
+ }
286
+
206
287
export {
207
288
proxyEventToWebRequest ,
208
289
webResponseToProxyResult ,
209
290
handlerResultToWebResponse ,
210
291
handlerResultToProxyResult ,
292
+ bodyToNodeStream ,
293
+ webHeadersToApiGatewayV1Headers ,
211
294
} ;
0 commit comments