serverless-offline 11.6.0 → 12.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "dedicatedTo": "Blue, a great migrating bird.",
3
3
  "name": "serverless-offline",
4
- "version": "11.6.0",
4
+ "version": "12.0.1",
5
5
  "description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -82,15 +82,15 @@
82
82
  ]
83
83
  },
84
84
  "dependencies": {
85
- "@aws-sdk/client-lambda": "^3.216.0",
85
+ "@aws-sdk/client-lambda": "^3.224.0",
86
86
  "@hapi/boom": "^10.0.0",
87
87
  "@hapi/h2o2": "^10.0.0",
88
- "@hapi/hapi": "^21.0.0",
88
+ "@hapi/hapi": "^21.1.0",
89
89
  "@serverless/utils": "^6.8.2",
90
90
  "boxen": "^7.0.0",
91
91
  "chalk": "^5.1.2",
92
92
  "execa": "^6.1.0",
93
- "fs-extra": "^10.1.0",
93
+ "fs-extra": "^11.1.0",
94
94
  "is-wsl": "^2.2.0",
95
95
  "java-invoke-local": "0.0.6",
96
96
  "jose": "^4.11.1",
@@ -98,7 +98,7 @@
98
98
  "jsonpath-plus": "^7.2.0",
99
99
  "jsonschema": "^1.4.1",
100
100
  "jszip": "^3.10.1",
101
- "luxon": "^3.1.0",
101
+ "luxon": "^3.1.1",
102
102
  "node-fetch": "^3.3.0",
103
103
  "node-schedule": "^2.1.0",
104
104
  "object.hasown": "^1.1.2",
@@ -110,18 +110,18 @@
110
110
  "devDependencies": {
111
111
  "@istanbuljs/esm-loader-hook": "^0.2.0",
112
112
  "archiver": "^5.3.1",
113
- "eslint": "^8.28.0",
113
+ "eslint": "^8.29.0",
114
114
  "eslint-config-airbnb-base": "^15.0.0",
115
115
  "eslint-config-prettier": "^8.5.0",
116
116
  "eslint-plugin-import": "^2.25.4",
117
117
  "eslint-plugin-prettier": "^4.2.1",
118
118
  "git-list-updated": "^1.2.1",
119
119
  "husky": "^8.0.2",
120
- "lint-staged": "^13.0.3",
120
+ "lint-staged": "^13.1.0",
121
121
  "mocha": "^10.1.0",
122
122
  "nyc": "^15.1.0",
123
123
  "prettier": "^2.8.0",
124
- "serverless": "^3.25.0",
124
+ "serverless": "^3.25.1",
125
125
  "standard-version": "^9.5.0"
126
126
  },
127
127
  "peerDependencies": {
@@ -9,6 +9,8 @@ import {
9
9
  import { gray } from './config/colors.js'
10
10
 
11
11
  export default class ServerlessOffline {
12
+ #alb = null
13
+
12
14
  #cliOptions = null
13
15
 
14
16
  #http = null
@@ -62,6 +64,7 @@ export default class ServerlessOffline {
62
64
  this.#mergeOptions()
63
65
 
64
66
  const {
67
+ albEvents,
65
68
  httpEvents,
66
69
  httpApiEvents,
67
70
  lambdas,
@@ -75,6 +78,10 @@ export default class ServerlessOffline {
75
78
 
76
79
  const eventModules = []
77
80
 
81
+ if (albEvents.length > 0) {
82
+ eventModules.push(this.#createAlb(albEvents))
83
+ }
84
+
78
85
  if (httpApiEvents.length > 0 || httpEvents.length > 0) {
79
86
  eventModules.push(this.#createHttp([...httpApiEvents, ...httpEvents]))
80
87
  }
@@ -104,6 +111,10 @@ export default class ServerlessOffline {
104
111
  eventModules.push(this.#lambda.stop(SERVER_SHUTDOWN_TIMEOUT))
105
112
  }
106
113
 
114
+ if (this.#alb) {
115
+ eventModules.push(this.#alb.stop(SERVER_SHUTDOWN_TIMEOUT))
116
+ }
117
+
107
118
  if (this.#http) {
108
119
  eventModules.push(this.#http.stop(SERVER_SHUTDOWN_TIMEOUT))
109
120
  }
@@ -217,6 +228,20 @@ export default class ServerlessOffline {
217
228
  await this.#webSocket.start()
218
229
  }
219
230
 
231
+ async #createAlb(events, skipStart) {
232
+ const { default: Alb } = await import('./events/alb/index.js')
233
+
234
+ this.#alb = new Alb(this.#serverless, this.#options, this.#lambda)
235
+
236
+ await this.#alb.createServer()
237
+
238
+ this.#alb.create(events)
239
+
240
+ if (!skipStart) {
241
+ await this.#alb.start()
242
+ }
243
+ }
244
+
220
245
  #mergeOptions() {
221
246
  const {
222
247
  service: { custom = {}, provider },
@@ -263,6 +288,7 @@ export default class ServerlessOffline {
263
288
  #getEvents() {
264
289
  const { service } = this.#serverless
265
290
 
291
+ const albEvents = []
266
292
  const httpEvents = []
267
293
  const httpApiEvents = []
268
294
  const lambdas = []
@@ -274,12 +300,23 @@ export default class ServerlessOffline {
274
300
  functionKeys.forEach((functionKey) => {
275
301
  const functionDefinition = service.getFunction(functionKey)
276
302
 
277
- lambdas.push({ functionDefinition, functionKey })
303
+ lambdas.push({
304
+ functionDefinition,
305
+ functionKey,
306
+ })
278
307
 
279
- const events = service.getAllEventsInFunction(functionKey) || []
308
+ const events = service.getAllEventsInFunction(functionKey) ?? []
280
309
 
281
310
  events.forEach((event) => {
282
- const { http, httpApi, schedule, websocket } = event
311
+ const { alb, http, httpApi, schedule, websocket } = event
312
+
313
+ if (alb) {
314
+ albEvents.push({
315
+ alb,
316
+ functionKey,
317
+ handler: functionDefinition.handler,
318
+ })
319
+ }
283
320
 
284
321
  if (http && functionDefinition.handler) {
285
322
  const httpEvent = {
@@ -369,6 +406,7 @@ export default class ServerlessOffline {
369
406
  })
370
407
 
371
408
  return {
409
+ albEvents,
372
410
  httpApiEvents,
373
411
  httpEvents,
374
412
  lambdas,
@@ -1,4 +1,8 @@
1
1
  export default {
2
+ albPort: {
3
+ type: 'string',
4
+ usage: 'ALB port to listen on. Default: 3003.',
5
+ },
2
6
  corsAllowHeaders: {
3
7
  type: 'string',
4
8
  usage:
@@ -1,4 +1,5 @@
1
1
  export default {
2
+ albPort: 3003,
2
3
  corsAllowHeaders: 'accept,content-type,x-api-key,authorization',
3
4
  corsAllowOrigin: '*',
4
5
  corsDisallowCredentials: true,
@@ -0,0 +1,56 @@
1
+ import { log } from '@serverless/utils/log.js'
2
+ import AlbEventDefinition from './AlbEventDefinition.js'
3
+ import HttpServer from './HttpServer.js'
4
+
5
+ export default class Alb {
6
+ #httpServer = null
7
+
8
+ #lambda = null
9
+
10
+ #options = null
11
+
12
+ #serverless = null
13
+
14
+ constructor(serverless, options, lambda) {
15
+ this.#lambda = lambda
16
+ this.#options = options
17
+ this.#serverless = serverless
18
+
19
+ log.warning(`
20
+ Application Load Balancer (ALB) support in serverless-offline is experimental.
21
+ Please file an issue for any bugs, missing features or other feedback: https://github.com/dherault/serverless-offline/issues
22
+ `)
23
+ }
24
+
25
+ start() {
26
+ return this.#httpServer.start()
27
+ }
28
+
29
+ stop(timeout) {
30
+ return this.#httpServer.stop(timeout)
31
+ }
32
+
33
+ async createServer() {
34
+ this.#httpServer = new HttpServer(
35
+ this.#serverless,
36
+ this.#options,
37
+ this.#lambda,
38
+ )
39
+
40
+ await this.#httpServer.createServer()
41
+ }
42
+
43
+ #createEvent(functionKey, rawAlbEventDefinition) {
44
+ const albEvent = new AlbEventDefinition(rawAlbEventDefinition)
45
+
46
+ this.#httpServer.createRoutes(functionKey, albEvent)
47
+ }
48
+
49
+ create(events) {
50
+ events.forEach(({ functionKey, alb }) => {
51
+ this.#createEvent(functionKey, alb)
52
+ })
53
+
54
+ this.#httpServer.writeRoutesTerminal()
55
+ }
56
+ }
@@ -0,0 +1,22 @@
1
+ const { assign } = Object
2
+
3
+ export default class AlbEventDefinition {
4
+ constructor(rawAlbEventDefinition) {
5
+ let listenerArn
6
+ let priority
7
+ let conditions
8
+ let rest
9
+
10
+ if (typeof rawAlbEventDefinition === 'string') {
11
+ ;[listenerArn, priority, conditions] = rawAlbEventDefinition.split(' ')
12
+ } else {
13
+ ;({ listenerArn, priority, conditions, ...rest } = rawAlbEventDefinition)
14
+ }
15
+
16
+ this.listenerArn = listenerArn
17
+ this.priority = priority
18
+ this.conditions = conditions
19
+
20
+ assign(this, rest)
21
+ }
22
+ }
@@ -0,0 +1,402 @@
1
+ import { Buffer } from 'node:buffer'
2
+ import { exit } from 'node:process'
3
+ import { Server } from '@hapi/hapi'
4
+ import { log } from '@serverless/utils/log.js'
5
+ import {
6
+ detectEncoding,
7
+ generateAlbHapiPath,
8
+ getHttpApiCorsConfig,
9
+ } from '../../utils/index.js'
10
+ import LambdaAlbRequestEvent from './lambda-events/LambdaAlbRequestEvent.js'
11
+ import logRoutes from '../../utils/logRoutes.js'
12
+
13
+ const { stringify } = JSON
14
+ const { entries } = Object
15
+
16
+ export default class HttpServer {
17
+ #lambda = null
18
+
19
+ #options = null
20
+
21
+ #serverless = null
22
+
23
+ #server = null
24
+
25
+ #lastRequestOptions = null
26
+
27
+ #terminalInfo = []
28
+
29
+ constructor(serverless, options, lambda) {
30
+ this.#serverless = serverless
31
+ this.#options = options
32
+ this.#lambda = lambda
33
+ }
34
+
35
+ async createServer() {
36
+ const { host, albPort } = this.#options
37
+
38
+ const serverOptions = {
39
+ host,
40
+ port: albPort,
41
+ router: {
42
+ // allows for paths with trailing slashes to be the same as without
43
+ // e.g. : /my-path is the same as /my-path/
44
+ stripTrailingSlash: true,
45
+ },
46
+ }
47
+
48
+ this.#server = new Server(serverOptions)
49
+
50
+ this.#server.ext('onPreResponse', (request, h) => {
51
+ if (request.headers.origin) {
52
+ const response = request.response.isBoom
53
+ ? request.response.output
54
+ : request.response
55
+
56
+ const explicitlySetHeaders = {
57
+ ...response.headers,
58
+ }
59
+
60
+ if (
61
+ this.#serverless.service.provider.httpApi &&
62
+ this.#serverless.service.provider.httpApi.cors
63
+ ) {
64
+ const httpApiCors = getHttpApiCorsConfig(
65
+ this.#serverless.service.provider.httpApi.cors,
66
+ this,
67
+ )
68
+
69
+ if (request.method === 'options') {
70
+ response.statusCode = 204
71
+ const allowAllOrigins =
72
+ httpApiCors.allowedOrigins.length === 1 &&
73
+ httpApiCors.allowedOrigins[0] === '*'
74
+ if (
75
+ !allowAllOrigins &&
76
+ !httpApiCors.allowedOrigins.includes(request.headers.origin)
77
+ ) {
78
+ return h.continue
79
+ }
80
+ }
81
+
82
+ response.headers['access-control-allow-origin'] =
83
+ request.headers.origin
84
+ if (httpApiCors.allowCredentials) {
85
+ response.headers['access-control-allow-credentials'] = 'true'
86
+ }
87
+ if (httpApiCors.maxAge) {
88
+ response.headers['access-control-max-age'] = httpApiCors.maxAge
89
+ }
90
+ if (httpApiCors.exposedResponseHeaders) {
91
+ response.headers['access-control-expose-headers'] =
92
+ httpApiCors.exposedResponseHeaders.join(',')
93
+ }
94
+ if (httpApiCors.allowedMethods) {
95
+ response.headers['access-control-allow-methods'] =
96
+ httpApiCors.allowedMethods.join(',')
97
+ }
98
+ if (httpApiCors.allowedHeaders) {
99
+ response.headers['access-control-allow-headers'] =
100
+ httpApiCors.allowedHeaders.join(',')
101
+ }
102
+ } else {
103
+ response.headers['access-control-allow-origin'] =
104
+ request.headers.origin
105
+ response.headers['access-control-allow-credentials'] = 'true'
106
+
107
+ if (request.method === 'options') {
108
+ response.statusCode = 200
109
+
110
+ if (request.headers['access-control-expose-headers']) {
111
+ response.headers['access-control-expose-headers'] =
112
+ request.headers['access-control-expose-headers']
113
+ } else {
114
+ response.headers['access-control-expose-headers'] =
115
+ 'content-type, content-length, etag'
116
+ }
117
+ response.headers['access-control-max-age'] = 60 * 10
118
+
119
+ if (request.headers['access-control-request-headers']) {
120
+ response.headers['access-control-allow-headers'] =
121
+ request.headers['access-control-request-headers']
122
+ }
123
+
124
+ if (request.headers['access-control-request-method']) {
125
+ response.headers['access-control-allow-methods'] =
126
+ request.headers['access-control-request-method']
127
+ }
128
+ }
129
+
130
+ // Override default headers with headers that have been explicitly set
131
+ entries(explicitlySetHeaders).forEach(([key, value]) => {
132
+ if (value) {
133
+ response.headers[key] = value
134
+ }
135
+ })
136
+ }
137
+ }
138
+ return h.continue
139
+ })
140
+ }
141
+
142
+ async start() {
143
+ const { host, albPort, httpsProtocol } = this.#options
144
+
145
+ try {
146
+ await this.#server.start()
147
+ } catch (err) {
148
+ log.error(
149
+ `Unexpected error while starting serverless-offline alb server on port ${albPort}:`,
150
+ err,
151
+ )
152
+ exit(1)
153
+ }
154
+
155
+ // TODO move the following block
156
+ const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${albPort}`
157
+
158
+ log.notice(`ALB Server ready: ${server} 🚀`)
159
+ }
160
+
161
+ stop(timeout) {
162
+ return this.#server.stop({
163
+ timeout,
164
+ })
165
+ }
166
+
167
+ get server() {
168
+ return this.#server.listener
169
+ }
170
+
171
+ #createHapiHandler(params) {
172
+ const { functionKey, method, stage } = params
173
+
174
+ return async (request, h) => {
175
+ this.#lastRequestOptions = {
176
+ headers: request.headers,
177
+ method: request.method,
178
+ payload: request.payload,
179
+ url: request.url.href,
180
+ }
181
+
182
+ const requestPath = this.#options.noPrependStageInUrl
183
+ ? request.path
184
+ : request.path.substr(`/${stage}`.length)
185
+
186
+ // Payload processing
187
+ const encoding = detectEncoding(request)
188
+
189
+ request.payload = request.payload && request.payload.toString(encoding)
190
+ request.rawPayload = request.payload
191
+
192
+ // Incoming request message
193
+ log.notice()
194
+
195
+ log.notice()
196
+ log.notice(`${method} ${request.path} (λ: ${functionKey})`)
197
+
198
+ const response = h.response()
199
+
200
+ let event = {}
201
+ try {
202
+ event = new LambdaAlbRequestEvent(request, stage, requestPath).create()
203
+ } catch (err) {
204
+ return this.#reply502(response, ``, err)
205
+ }
206
+
207
+ log.debug('event:', event)
208
+
209
+ const lambdaFunction = this.#lambda.get(functionKey)
210
+
211
+ lambdaFunction.setEvent(event)
212
+
213
+ let result
214
+ let err
215
+
216
+ try {
217
+ result = await lambdaFunction.runHandler()
218
+ } catch (_err) {
219
+ err = _err
220
+ }
221
+
222
+ log.debug('_____ HANDLER RESOLVED _____')
223
+
224
+ // Failure handling
225
+ let errorStatusCode = '502'
226
+
227
+ if (err) {
228
+ const errorMessage = (err.message || err).toString()
229
+
230
+ const found = errorMessage.match(/\[(\d{3})]/)
231
+
232
+ if (found && found.length > 1) {
233
+ ;[, errorStatusCode] = found
234
+ } else {
235
+ errorStatusCode = '502'
236
+ }
237
+
238
+ // Mocks Lambda errors
239
+ result = {
240
+ errorMessage,
241
+ errorType: err.constructor.name,
242
+ stackTrace: this.#getArrayStackTrace(err.stack),
243
+ }
244
+
245
+ log.error(errorMessage)
246
+ }
247
+
248
+ let statusCode = 200
249
+
250
+ if (result && !result.errorType) {
251
+ statusCode = result.statusCode || 200
252
+ } else if (err) {
253
+ statusCode = errorStatusCode || 502
254
+ } else {
255
+ statusCode = 502
256
+ }
257
+
258
+ response.statusCode = statusCode
259
+
260
+ const headers = {}
261
+
262
+ if (result && result.headers) {
263
+ entries(result.headers).forEach(([headerKey, headerValue]) => {
264
+ headers[headerKey] = (headers[headerKey] || []).concat(headerValue)
265
+ })
266
+ }
267
+
268
+ if (result && result.multiValueHeaders) {
269
+ entries(result.multiValueHeaders).forEach(
270
+ ([headerKey, headerValue]) => {
271
+ headers[headerKey] = (headers[headerKey] || []).concat(headerValue)
272
+ },
273
+ )
274
+ }
275
+
276
+ log.debug('headers:', headers)
277
+
278
+ response.header('Content-Type', 'application/json', {
279
+ duplicate: false,
280
+ override: false,
281
+ })
282
+
283
+ if (typeof result === 'string') {
284
+ response.source = stringify(result)
285
+ } else if (result && result.body !== undefined) {
286
+ if (result.isBase64Encoded) {
287
+ response.encoding = 'binary'
288
+ response.source = Buffer.from(result.body, 'base64')
289
+ response.variety = 'buffer'
290
+ } else {
291
+ if (result && result.body && typeof result.body !== 'string') {
292
+ // FIXME TODO we should probably just write to console instead of returning a payload
293
+ return this.#reply502(
294
+ response,
295
+ 'According to the API Gateway specs, the body content must be stringified. Check your Lambda response and make sure you are invoking JSON.stringify(YOUR_CONTENT) on your body object',
296
+ {},
297
+ )
298
+ }
299
+ response.source = result.body
300
+ }
301
+ }
302
+
303
+ return response
304
+ }
305
+ }
306
+
307
+ createRoutes(functionKey, albEvent) {
308
+ const method = albEvent.conditions.method[0].toUpperCase()
309
+ const path = albEvent.conditions.path[0]
310
+ const hapiPath = generateAlbHapiPath(path, this.#options, this.#serverless)
311
+
312
+ const stage = this.#options.stage || this.#serverless.service.provider.stage
313
+ const { host, albPort, httpsProtocol } = this.#options
314
+ const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${albPort}`
315
+
316
+ this.#terminalInfo.push({
317
+ invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
318
+ method,
319
+ path: hapiPath,
320
+ server,
321
+ stage: this.#options.noPrependStageInUrl ? null : stage,
322
+ })
323
+
324
+ const hapiMethod = method === 'ANY' ? '*' : method
325
+ const hapiOptions = {}
326
+
327
+ // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
328
+ // for more details, check https://github.com/dherault/serverless-offline/issues/204
329
+ if (hapiMethod === 'HEAD') {
330
+ log.notice(
331
+ 'HEAD method event detected. Skipping HAPI server route mapping',
332
+ )
333
+
334
+ return
335
+ }
336
+
337
+ if (hapiMethod !== 'HEAD' && hapiMethod !== 'GET') {
338
+ // maxBytes: Increase request size from 1MB default limit to 10MB.
339
+ // Cf AWS API GW payload limits.
340
+ hapiOptions.payload = {
341
+ maxBytes: 1024 * 1024 * 10,
342
+ parse: false,
343
+ }
344
+ }
345
+
346
+ const hapiHandler = this.#createHapiHandler({
347
+ functionKey,
348
+ method,
349
+ stage,
350
+ })
351
+
352
+ this.#server.route({
353
+ handler: hapiHandler,
354
+ method: hapiMethod,
355
+ options: hapiOptions,
356
+ path: hapiPath,
357
+ })
358
+ }
359
+
360
+ #replyError(statusCode, response, message, error) {
361
+ log.notice(message)
362
+
363
+ log.error(error)
364
+
365
+ response.header('Content-Type', 'application/json')
366
+
367
+ response.statusCode = statusCode
368
+ response.source = {
369
+ errorMessage: message,
370
+ errorType: error.constructor.name,
371
+ offlineInfo:
372
+ 'If you believe this is an issue with serverless-offline please submit it, thanks. https://github.com/dherault/serverless-offline/issues',
373
+ stackTrace: this.#getArrayStackTrace(error.stack),
374
+ }
375
+
376
+ return response
377
+ }
378
+
379
+ #reply502(response, message, error) {
380
+ // APIG replies 502 by default on failures;
381
+ return this.#replyError(502, response, message, error)
382
+ }
383
+
384
+ #getArrayStackTrace(stack) {
385
+ if (!stack) return null
386
+
387
+ const splittedStack = stack.split('\n')
388
+
389
+ return splittedStack
390
+ .slice(
391
+ 0,
392
+ splittedStack.findIndex((item) =>
393
+ item.match(/server.route.handler.LambdaContext/),
394
+ ),
395
+ )
396
+ .map((line) => line.trim())
397
+ }
398
+
399
+ writeRoutesTerminal() {
400
+ logRoutes(this.#terminalInfo)
401
+ }
402
+ }
@@ -0,0 +1 @@
1
+ export { default } from './Alb.js'
@@ -0,0 +1,52 @@
1
+ import {
2
+ parseMultiValueHeaders,
3
+ parseMultiValueQueryStringParameters,
4
+ } from '../../../utils/index.js'
5
+
6
+ const { fromEntries } = Object
7
+
8
+ export default class LambdaAlbRequestEvent {
9
+ #path = null
10
+
11
+ #request = null
12
+
13
+ #stage = null
14
+
15
+ constructor(request, stage, path) {
16
+ this.#path = path
17
+ this.#request = request
18
+ this.#stage = stage
19
+ }
20
+
21
+ create() {
22
+ const { method } = this.#request
23
+ const { rawHeaders, url } = this.#request.raw.req
24
+ const httpMethod = method.toUpperCase()
25
+
26
+ const queryStringParameters = this.#request.url.search
27
+ ? fromEntries(Array.from(this.#request.url.searchParams))
28
+ : null
29
+
30
+ return {
31
+ body: this.#request.payload,
32
+ headers: this.#request.headers,
33
+ httpMethod,
34
+ isBase64Encoded: false,
35
+ multiValueHeaders: parseMultiValueHeaders(
36
+ // NOTE FIXME request.raw.req.rawHeaders can only be null for testing (hapi shot inject())
37
+ rawHeaders || [],
38
+ ),
39
+ multiValueQueryStringParameters:
40
+ parseMultiValueQueryStringParameters(url),
41
+ path: this.#path,
42
+ queryStringParameters,
43
+ requestContext: {
44
+ elb: {
45
+ targetGroupArn:
46
+ // TODO: probably replace this
47
+ 'arn:aws:elasticloadbalancing:us-east-1:550213415212:targetgroup/5811b5d6aff964cd50efa8596604c4e0/b49d49c443aa999f',
48
+ },
49
+ },
50
+ }
51
+ }
52
+ }
@@ -0,0 +1 @@
1
+ export { default } from './LambdaAlbRequestEvent.js'
@@ -828,6 +828,8 @@ export default class HttpServer {
828
828
 
829
829
  if (result && !result.errorType) {
830
830
  statusCode = result.statusCode || 200
831
+ } else if (err) {
832
+ statusCode = errorStatusCode || 502
831
833
  } else {
832
834
  statusCode = 502
833
835
  }
@@ -280,7 +280,7 @@ export default class LambdaFunction {
280
280
  async #timeoutAndTerminate() {
281
281
  await setTimeoutPromise(this.#timeout)
282
282
 
283
- throw new LambdaTimeoutError('Lambda timeout.')
283
+ throw new LambdaTimeoutError('[504] - Lambda timeout.')
284
284
  }
285
285
 
286
286
  async runHandler() {
@@ -1,7 +1,7 @@
1
1
  import { spawn } from 'node:child_process'
2
2
  import { EOL, platform } from 'node:os'
3
3
  import { delimiter, dirname, join, relative, resolve } from 'node:path'
4
- import process, { cwd } from 'node:process'
4
+ import process, { cwd, nextTick } from 'node:process'
5
5
  import { createInterface } from 'node:readline'
6
6
  import { fileURLToPath } from 'node:url'
7
7
  import { log } from '@serverless/utils/log.js'
@@ -99,7 +99,7 @@ export default class PythonRunner {
99
99
  // invoke.py, based on:
100
100
  // https://github.com/serverless/serverless/blob/v1.50.0/lib/plugins/aws/invokeLocal/invoke.py
101
101
  async run(event, context) {
102
- return new Promise((accept, reject) => {
102
+ return new Promise((res, rej) => {
103
103
  const input = stringify({
104
104
  context,
105
105
  event,
@@ -117,18 +117,17 @@ export default class PythonRunner {
117
117
  if (parsed) {
118
118
  this.#handlerProcess.stdout.readline.removeListener('line', onLine)
119
119
  this.#handlerProcess.stderr.removeListener('data', onErr)
120
- return accept(parsed)
120
+ res(parsed)
121
121
  }
122
- return null
123
122
  } catch (err) {
124
- return reject(err)
123
+ rej(err)
125
124
  }
126
125
  }
127
126
 
128
127
  this.#handlerProcess.stdout.readline.on('line', onLine)
129
128
  this.#handlerProcess.stderr.on('data', onErr)
130
129
 
131
- process.nextTick(() => {
130
+ nextTick(() => {
132
131
  this.#handlerProcess.stdin.write(input)
133
132
  this.#handlerProcess.stdin.write('\n')
134
133
  })
@@ -19,3 +19,32 @@ export default function generateHapiPath(path, options, serverless) {
19
19
 
20
20
  return hapiPath
21
21
  }
22
+
23
+ export function generateAlbHapiPath(path, options, serverless) {
24
+ // path must start with '/'
25
+ let hapiPath = path.startsWith('/') ? path : `/${path}`
26
+
27
+ if (!options.noPrependStageInUrl) {
28
+ const stage = options.stage || serverless.service.provider.stage
29
+ // prepend the stage to path
30
+ hapiPath = `/${stage}${hapiPath}`
31
+ }
32
+
33
+ if (options.prefix) {
34
+ hapiPath = `/${options.prefix}${hapiPath}`
35
+ }
36
+
37
+ if (
38
+ hapiPath !== '/' &&
39
+ hapiPath.endsWith('/') &&
40
+ !options.noStripTrailingSlashInUrl
41
+ ) {
42
+ hapiPath = hapiPath.slice(0, -1)
43
+ }
44
+
45
+ for (let i = 0; hapiPath.includes('*'); i += 1) {
46
+ hapiPath = hapiPath.replace('*', `{${i}}`)
47
+ }
48
+
49
+ return hapiPath
50
+ }
@@ -16,7 +16,7 @@ export { default as parseMultiValueQueryStringParameters } from './parseMultiVal
16
16
  export { default as parseQueryStringParameters } from './parseQueryStringParameters.js'
17
17
  export { default as parseQueryStringParametersForPayloadV2 } from './parseQueryStringParametersForPayloadV2.js'
18
18
  export { default as splitHandlerPathAndName } from './splitHandlerPathAndName.js'
19
-
19
+ export { generateAlbHapiPath } from './generateHapiPath.js'
20
20
  // export { default as baseImage } from './baseImage.js'
21
21
 
22
22
  const { isArray } = Array