serverless-offline 9.1.0 → 9.1.3

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/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
  <a href="https://www.npmjs.com/package/serverless-offline">
8
8
  <img src="https://img.shields.io/npm/v/serverless-offline.svg?style=flat-square">
9
9
  </a>
10
- <a href="https://github.com/dherault/serverless-offline/actions?query=workflow%3ACI">
11
- <img src="https://img.shields.io/github/workflow/status/dherault/serverless-offline/CI?style=flat-square">
10
+ <a href="https://github.com/dherault/serverless-offline/actions/workflows/integrate.yml">
11
+ <img src="https://img.shields.io/github/workflow/status/dherault/serverless-offline/Integrate">
12
12
  </a>
13
13
  <img src="https://img.shields.io/node/v/serverless-offline.svg?style=flat-square">
14
14
  <a href="https://github.com/serverless/serverless">
@@ -29,9 +29,9 @@
29
29
  This [Serverless](https://github.com/serverless/serverless) plugin emulates [AWS λ](https://aws.amazon.com/lambda) and [API Gateway](https://aws.amazon.com/api-gateway) on your local machine to speed up your development cycles.
30
30
  To do so, it starts an HTTP server that handles the request's lifecycle like APIG does and invokes your handlers.
31
31
 
32
- **Features:**
32
+ **Features**
33
33
 
34
- - [Node.js](https://nodejs.org), [Python](https://www.python.org), [Ruby](https://www.ruby-lang.org) and [Go](https://golang.org) λ runtimes.
34
+ - [Node.js](https://nodejs.org), [Python](https://www.python.org), [Ruby](https://www.ruby-lang.org), [Go](https://golang.org), [Java](https://www.java.com) (incl. [Kotlin](https://kotlinlang.org), [Groovy](https://groovy-lang.org), [Scala](https://www.scala-lang.org)) λ runtimes.
35
35
  - Velocity templates support.
36
36
  - Lazy loading of your handler files.
37
37
  - And more: integrations, authorizers, proxies, timeouts, responseParameters, HTTPS, CORS, etc...
@@ -42,6 +42,7 @@ This plugin is updated by its users, I just do maintenance and ensure that PRs a
42
42
 
43
43
  - [Installation](#installation)
44
44
  - [Usage and command line options](#usage-and-command-line-options)
45
+ - [Run modes](#run-modes)
45
46
  - [Usage with `invoke`](#usage-with-invoke)
46
47
  - [The `process.env.IS_OFFLINE` variable](#the-processenvis_offline-variable)
47
48
  - [Docker and Layers](#docker-and-layers)
@@ -84,7 +85,7 @@ Then inside your project's `serverless.yml` file add following entry to the plug
84
85
 
85
86
  It should look something like this:
86
87
 
87
- ```YAML
88
+ ```yml
88
89
  plugins:
89
90
  - serverless-offline
90
91
  ```
@@ -146,13 +147,12 @@ All CLI options are optional:
146
147
 
147
148
  Any of the CLI options can be added to your `serverless.yml`. For example:
148
149
 
149
- ```
150
+ ```yml
150
151
  custom:
151
152
  serverless-offline:
152
- httpsProtocol: "dev-certs"
153
+ httpsProtocol: 'dev-certs'
153
154
  httpPort: 4000
154
- stageVariables:
155
- foo: "bar"
155
+ foo: 'bar'
156
156
  ```
157
157
 
158
158
  Options passed on the command line override YAML options.
@@ -164,18 +164,64 @@ By default you can send your requests to `http://localhost:3000/`. Please note t
164
164
  But if you send an `application/x-www-form-urlencoded` or a `multipart/form-data` body with an `application/json` (or no) Content-Type, API Gateway won't parse your data (you'll get the ugly raw as input), whereas the plugin will answer 400 (malformed JSON).
165
165
  Please consider explicitly setting your requests' Content-Type and using separate templates.
166
166
 
167
+ ## Run modes
168
+
169
+ ### node.js
170
+
171
+ Lambda handlers for the `node.js` runtime can run in different execution modes with `serverless-offline` and they have subtle differences with a variety of pros and cons. they are mutually exclusive and it is planned to combine the flags into one single flag in the future.
172
+
173
+ #### worker-threads (default)
174
+
175
+ - handlers run in their own context
176
+ - memory is not being shared between handlers, memory consumption is therefore higher
177
+ - memory is being released when handlers reload or after usage
178
+ - environment (process.env) is not being shared across handlers
179
+ - global state is not being shared across handlers
180
+ - easy debugging
181
+
182
+ #### in-process
183
+
184
+ - handlers run in the same context (instance) as `serverless` and `serverless-offline`
185
+ - memory is being shared across lambda handlers as well as with `serverless` and `serverless-offline`
186
+ - no reloading capabilities as it is [currently] not possible to implement for commonjs handlers (without memory leaks) and for esm handlers
187
+ - environment (process.env) is being shared across handlers as well as with `serverless` and `serverless-offline`
188
+ - global state is being shared across lambda handlers as well as with `serverless` and `serverless-offline`
189
+ - easy debugging
190
+
191
+ #### child-processes
192
+
193
+ - handlers run in a separate node.js instance
194
+ - memory is not being shared between handlers, memory consumption is therefore higher
195
+ - memory is being released when handlers reload or after usage
196
+ - environment (process.env) is not being shared across handlers
197
+ - global state is not being shared across handlers
198
+ - debugging more complicated
199
+
200
+ #### docker
201
+
202
+ - handlers run in a docker container
203
+ - memory is not being shared between handlers, memory consumption is therefore higher
204
+ - memory is being released when handlers reload or after usage
205
+ - environment (process.env) is not being shared across handlers
206
+ - global state is not being shared across handlers
207
+ - debugging more complicated
208
+
209
+ ### Python, Ruby, Go, Java (incl. Kotlin, Groovy, Scala)
210
+
211
+ the Lambda handler process is running in a child process.
212
+
167
213
  ## Usage with `invoke`
168
214
 
169
- To use `Lambda.invoke` you need to set the lambda endpoint to the serverless-offline endpoint:
215
+ To use `Lambda.invoke` you need to set the lambda endpoint to the `serverless-offline` endpoint:
170
216
 
171
217
  ```js
218
+ const { env } = require('node:process')
172
219
  const { Lambda } = require('aws-sdk')
173
220
 
174
221
  const lambda = new Lambda({
175
222
  apiVersion: '2015-03-31',
176
- // endpoint needs to be set only if it deviates from the default, e.g. in a dev environment
177
- // process.env.SOME_VARIABLE could be set in e.g. serverless.yml for provider.environment or function.environment
178
- endpoint: process.env.SOME_VARIABLE
223
+ // endpoint needs to be set only if it deviates from the default
224
+ endpoint: env.IS_OFFLINE
179
225
  ? 'http://localhost:3002'
180
226
  : 'https://lambda.us-east-1.amazonaws.com',
181
227
  })
@@ -184,15 +230,33 @@ const lambda = new Lambda({
184
230
  All your lambdas can then be invoked in a handler using
185
231
 
186
232
  ```js
233
+ const { Buffer } = require('node:buffer')
234
+ const { Lambda } = require('aws-sdk')
235
+
236
+ const { stringify } = JSON
237
+
238
+ const lambda = new Lambda({
239
+ apiVersion: '2015-03-31',
240
+ endpoint: 'http://localhost:3002',
241
+ })
242
+
187
243
  exports.handler = async function () {
244
+ const clientContextData = stringify({ foo: 'foo' })
245
+
188
246
  const params = {
247
+ ClientContext: Buffer.from(clientContextData).toString('base64'),
189
248
  // FunctionName is composed of: service name - stage - function name, e.g.
190
249
  FunctionName: 'myServiceName-dev-invokedHandler',
191
250
  InvocationType: 'RequestResponse',
192
- Payload: JSON.stringify({ data: 'foo' }),
251
+ Payload: stringify({ data: 'foo' }),
193
252
  }
194
253
 
195
254
  const response = await lambda.invoke(params).promise()
255
+
256
+ return {
257
+ body: stringify(response),
258
+ statusCode: 200,
259
+ }
196
260
  }
197
261
  ```
198
262
 
@@ -244,7 +308,7 @@ to calling it via `aws-sdk`.
244
308
 
245
309
  ## The `process.env.IS_OFFLINE` variable
246
310
 
247
- Will be `"true"` in your handlers and throughout the plugin.
311
+ Will be `"true"` in your handlers when using `serverless-offline`.
248
312
 
249
313
  ## Docker and Layers
250
314
 
@@ -326,11 +390,11 @@ Only [custom authorizers](https://aws.amazon.com/blogs/compute/introducing-custo
326
390
 
327
391
  The Custom authorizer is passed an `event` object as below:
328
392
 
329
- ```javascript
393
+ ```js
330
394
  {
331
- "type": "TOKEN",
332
395
  "authorizationToken": "<Incoming bearer token>",
333
- "methodArn": "arn:aws:execute-api:<Region id>:<Account id>:<API id>/<Stage>/<Method>/<Resource path>"
396
+ "methodArn": "arn:aws:execute-api:<Region id>:<Account id>:<API id>/<Stage>/<Method>/<Resource path>",
397
+ "type": "TOKEN"
334
398
  }
335
399
  ```
336
400
 
@@ -338,11 +402,11 @@ The `methodArn` does not include the Account id or API id.
338
402
 
339
403
  The plugin only supports retrieving Tokens from headers. You can configure the header as below:
340
404
 
341
- ```javascript
405
+ ```js
342
406
  "authorizer": {
343
- "type": "TOKEN",
407
+ "authorizerResultTtlInSeconds": "0",
344
408
  "identitySource": "method.request.header.Authorization", // or method.request.header.SomeOtherHeader
345
- "authorizerResultTtlInSeconds": "0"
409
+ "type": "TOKEN"
346
410
  }
347
411
  ```
348
412
 
@@ -370,14 +434,14 @@ If your authentication needs are custom and not satisfied by the existing capabi
370
434
  ```js
371
435
  module.exports = function (endpoint, functionKey, method, path) {
372
436
  return {
373
- name: 'your strategy name',
374
- scheme: 'your scheme name',
375
-
376
437
  getAuthenticateFunction: () => ({
377
438
  async authenticate(request, h) {
378
439
  // your implementation
379
440
  },
380
441
  }),
442
+
443
+ name: 'your strategy name',
444
+ scheme: 'your scheme name',
381
445
  }
382
446
  }
383
447
  ```
@@ -441,7 +505,7 @@ Now let's make a request with this body: `{ "id": 1 }`
441
505
 
442
506
  AWS parses the event as such:
443
507
 
444
- ```javascript
508
+ ```js
445
509
  {
446
510
  "payload": {
447
511
  "id": 1
@@ -453,7 +517,7 @@ AWS parses the event as such:
453
517
 
454
518
  Whereas Offline parses:
455
519
 
456
- ```javascript
520
+ ```js
457
521
  {
458
522
  "payload": {
459
523
  "id": 1
@@ -505,7 +569,7 @@ Works out of the box. See examples in the manual_test directory.
505
569
 
506
570
  Example of enabling proxy:
507
571
 
508
- ```
572
+ ```yml
509
573
  custom:
510
574
  serverless-offline:
511
575
  resourceRoutes: true
@@ -513,18 +577,18 @@ custom:
513
577
 
514
578
  or
515
579
 
516
- ```
580
+ ```yml
517
581
  YourCloudFormationMethodId:
518
- Type: AWS::ApiGateway::Method
519
582
  Properties:
520
583
  ......
521
584
  Integration:
522
585
  Type: HTTP_PROXY
523
586
  Uri: 'https://s3-${self:custom.region}.amazonaws.com/${self:custom.yourBucketName}/{proxy}'
524
587
  ......
588
+ Type: AWS::ApiGateway::Method
525
589
  ```
526
590
 
527
- ```
591
+ ```yml
528
592
  custom:
529
593
  serverless-offline:
530
594
  resourceRoutes:
@@ -542,7 +606,7 @@ May not work properly. Please PR. (Difficulty: hard?)
542
606
 
543
607
  Example response velocity template:
544
608
 
545
- ```javascript
609
+ ```js
546
610
  "responseParameters": {
547
611
  "method.response.header.X-Powered-By": "Serverless", // a string
548
612
  "method.response.header.Warning": "integration.response.body", // the whole response
@@ -559,7 +623,9 @@ Usage in order to send messages back to clients:
559
623
  Or,
560
624
 
561
625
  ```js
562
- const apiGatewayManagementApi = new AWS.ApiGatewayManagementApi({
626
+ const { ApiGatewayManagementApi } = require('aws-sdk')
627
+
628
+ const apiGatewayManagementApi = new ApiGatewayManagementApi({
563
629
  apiVersion: '2018-11-29',
564
630
  endpoint: 'http://localhost:3001',
565
631
  });
@@ -648,7 +714,7 @@ You can change this profile directly in the code or by setting proper environmen
648
714
  ## Simulation quality
649
715
 
650
716
  This plugin simulates API Gateway for many practical purposes, good enough for development - but is not a perfect simulator.
651
- Specifically, Lambda currently runs on Node.js v10.x, v12.x and v14.x ([AWS Docs](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html)), whereas _Offline_ runs on your own runtime where no memory limits are enforced.
717
+ Specifically, Lambda currently runs on Node.js v12.x, v14.x and v16.x ([AWS Docs](https://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html)), whereas _Offline_ runs on your own runtime where no memory limits are enforced.
652
718
 
653
719
  ## Usage with other plugins
654
720
 
@@ -661,7 +727,7 @@ Plugins are executed in order, so plugins that process your code or add resource
661
727
 
662
728
  For example:
663
729
 
664
- ```yaml
730
+ ```yml
665
731
  plugins:
666
732
  - serverless-middleware # modifies some of your handler based on configuration
667
733
  - serverless-webpack # package your javascript handlers using webpack
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": "9.1.0",
4
+ "version": "9.1.3",
5
5
  "description": "Emulate AWS λ and API Gateway locally when developing your Serverless project",
6
6
  "license": "MIT",
7
7
  "main": "./src/index.js",
@@ -194,7 +194,7 @@
194
194
  "@hapi/boom": "^10.0.0",
195
195
  "@hapi/h2o2": "^9.1.0",
196
196
  "@hapi/hapi": "^20.2.2",
197
- "aws-sdk": "^2.1181.0",
197
+ "aws-sdk": "^2.1186.0",
198
198
  "boxen": "^7.0.0",
199
199
  "chalk": "^5.0.1",
200
200
  "execa": "^6.1.0",
@@ -206,7 +206,7 @@
206
206
  "jsonwebtoken": "^8.5.1",
207
207
  "jszip": "^3.10.0",
208
208
  "luxon": "^3.0.1",
209
- "node-fetch": "^3.2.9",
209
+ "node-fetch": "^3.2.10",
210
210
  "node-schedule": "^2.1.0",
211
211
  "object.hasown": "^1.1.1",
212
212
  "p-memoize": "^7.1.0",
@@ -216,7 +216,7 @@
216
216
  },
217
217
  "devDependencies": {
218
218
  "archiver": "^5.3.1",
219
- "eslint": "^8.20.0",
219
+ "eslint": "^8.21.0",
220
220
  "eslint-config-airbnb-base": "^15.0.0",
221
221
  "eslint-config-prettier": "^8.5.0",
222
222
  "eslint-plugin-import": "^2.25.4",
@@ -168,7 +168,7 @@ export default class ServerlessOffline {
168
168
 
169
169
  this.#http = new Http(this.#serverless, this.#options, this.#lambda)
170
170
 
171
- await this.#http.registerPlugins()
171
+ await this.#http.createServer()
172
172
 
173
173
  this.#http.create(events)
174
174
 
@@ -236,12 +236,8 @@ export default class ServerlessOffline {
236
236
  .replace(/\s/g, '')
237
237
  .split(',')
238
238
 
239
- if (this.#options.corsDisallowCredentials) {
240
- this.#options.corsAllowCredentials = false
241
- }
242
-
243
239
  this.#options.corsConfig = {
244
- credentials: this.#options.corsAllowCredentials,
240
+ credentials: !this.#options.corsDisallowCredentials,
245
241
  exposedHeaders: this.#options.corsExposedHeaders,
246
242
  headers: this.#options.corsAllowHeaders,
247
243
  origin: this.#options.corsAllowOrigin,
@@ -9,7 +9,7 @@ export const DEFAULT_LAMBDA_RUNTIME = 'nodejs14.x'
9
9
  // https://docs.aws.amazon.com/lambda/latest/dg/limits.html
10
10
  export const DEFAULT_LAMBDA_MEMORY_SIZE = 1024
11
11
  // default function timeout in seconds
12
- export const DEFAULT_LAMBDA_TIMEOUT = 900 // 15 min
12
+ export const DEFAULT_LAMBDA_TIMEOUT = 6 // 6 seconds
13
13
 
14
14
  // timeout for all connections to be closed
15
15
  export const SERVER_SHUTDOWN_TIMEOUT = 5000
@@ -2,9 +2,9 @@ import { createApiKey } from '../utils/index.js'
2
2
 
3
3
  export default {
4
4
  apiKey: createApiKey(),
5
- corsAllowCredentials: true, // TODO no CLI option
6
5
  corsAllowHeaders: 'accept,content-type,x-api-key,authorization',
7
6
  corsAllowOrigin: '*',
7
+ corsDisallowCredentials: true,
8
8
  corsExposedHeaders: 'WWW-Authenticate,Server-Authorization',
9
9
  disableCookieValidation: false,
10
10
  disableScheduledEvents: false,
@@ -17,6 +17,10 @@ export default class Http {
17
17
  return this.#httpServer.stop(timeout)
18
18
  }
19
19
 
20
+ async createServer() {
21
+ await this.#httpServer.createServer()
22
+ }
23
+
20
24
  #createEvent(functionKey, rawHttpEventDefinition, handler) {
21
25
  const httpEvent = new HttpEventDefinition(rawHttpEventDefinition)
22
26
 
@@ -39,10 +43,6 @@ export default class Http {
39
43
  this.#httpServer.create404Route()
40
44
  }
41
45
 
42
- registerPlugins() {
43
- return this.#httpServer.registerPlugins()
44
- }
45
-
46
46
  // TEMP FIXME quick fix to expose gateway server for testing, look for better solution
47
47
  getServer() {
48
48
  return this.#httpServer.getServer()
@@ -1,5 +1,5 @@
1
1
  import { Buffer } from 'node:buffer'
2
- import { readFileSync } from 'node:fs'
2
+ import { readFile } from 'node:fs/promises'
3
3
  import { createRequire } from 'node:module'
4
4
  import { join, resolve } from 'node:path'
5
5
  import { exit } from 'node:process'
@@ -47,7 +47,9 @@ export default class HttpServer {
47
47
  this.#lambda = lambda
48
48
  this.#options = options
49
49
  this.#serverless = serverless
50
+ }
50
51
 
52
+ async createServer() {
51
53
  const {
52
54
  enforceSecureCookies,
53
55
  host,
@@ -80,14 +82,20 @@ export default class HttpServer {
80
82
  // HTTPS support
81
83
  if (typeof httpsProtocol === 'string' && httpsProtocol.length > 0) {
82
84
  serverOptions.tls = {
83
- cert: readFileSync(resolve(httpsProtocol, 'cert.pem'), 'ascii'),
84
- key: readFileSync(resolve(httpsProtocol, 'key.pem'), 'ascii'),
85
+ cert: readFile(resolve(httpsProtocol, 'cert.pem'), 'ascii'),
86
+ key: readFile(resolve(httpsProtocol, 'key.pem'), 'ascii'),
85
87
  }
86
88
  }
87
89
 
88
90
  // Hapijs server creation
89
91
  this.#server = new Server(serverOptions)
90
92
 
93
+ try {
94
+ await this.#server.register([h2o2])
95
+ } catch (err) {
96
+ log.error(err)
97
+ }
98
+
91
99
  // Enable CORS preflight response
92
100
  this.#server.ext('onPreResponse', (request, h) => {
93
101
  if (request.headers.origin) {
@@ -205,14 +213,6 @@ export default class HttpServer {
205
213
  })
206
214
  }
207
215
 
208
- async registerPlugins() {
209
- try {
210
- await this.#server.register([h2o2])
211
- } catch (err) {
212
- log.error(err)
213
- }
214
- }
215
-
216
216
  #logPluginIssue() {
217
217
  log.notice(
218
218
  'If you think this is an issue with the plugin please submit it, thanks!\nhttps://github.com/dherault/serverless-offline/issues',
@@ -260,7 +260,7 @@ export default class HttpServer {
260
260
  log.debug(`Creating Authorization scheme for ${authKey}`)
261
261
 
262
262
  // Create the Auth Scheme for the endpoint
263
- const scheme = createJWTAuthScheme(jwtSettings, this)
263
+ const scheme = createJWTAuthScheme(jwtSettings)
264
264
 
265
265
  // Set the auth scheme and strategy on the server
266
266
  this.#server.auth.scheme(authSchemeName, scheme)
@@ -338,6 +338,7 @@ export default class HttpServer {
338
338
  * /tests/integration/custom-authentication
339
339
  */
340
340
  const customizations = this.#serverless.service.custom
341
+
341
342
  if (
342
343
  customizations &&
343
344
  customizations.offline?.customAuthenticationProvider
@@ -350,11 +351,13 @@ export default class HttpServer {
350
351
  )
351
352
 
352
353
  const strategy = provider(endpoint, functionKey, method, path)
354
+
353
355
  this.#server.auth.scheme(
354
356
  strategy.scheme,
355
357
  strategy.getAuthenticateFunction,
356
358
  )
357
359
  this.#server.auth.strategy(strategy.name, strategy.scheme)
360
+
358
361
  return strategy.name
359
362
  }
360
363
 
@@ -363,164 +366,44 @@ export default class HttpServer {
363
366
  ? null
364
367
  : this.#configureJWTAuthorization(endpoint, functionKey, method, path) ||
365
368
  this.#configureAuthorization(endpoint, functionKey, method, path)
369
+
366
370
  return authStrategyName
367
371
  }
368
372
 
369
- createRoutes(functionKey, httpEvent, handler) {
370
- const [handlerPath] = splitHandlerPathAndName(handler)
371
-
372
- let method
373
- let path
374
- let hapiPath
375
-
376
- if (httpEvent.isHttpApi) {
377
- if (httpEvent.routeKey === '$default') {
378
- method = 'ANY'
379
- path = httpEvent.routeKey
380
- hapiPath = '/{default*}'
381
- } else {
382
- ;[method, path] = httpEvent.routeKey.split(' ')
383
- hapiPath = generateHapiPath(
384
- path,
385
- {
386
- ...this.#options,
387
- noPrependStageInUrl: true, // Serverless always uses the $default stage
388
- },
389
- this.#serverless,
390
- )
391
- }
392
- } else {
393
- method = httpEvent.method.toUpperCase()
394
- ;({ path } = httpEvent)
395
- hapiPath = generateHapiPath(path, this.#options, this.#serverless)
396
- }
397
-
398
- const endpoint = new Endpoint(
399
- join(this.#serverless.config.servicePath, handlerPath),
400
- httpEvent,
401
- ).generate()
402
-
403
- const stage = endpoint.isHttpApi
404
- ? '$default'
405
- : this.#options.stage || this.#serverless.service.provider.stage
406
- const protectedRoutes = []
407
-
408
- if (httpEvent.private) {
409
- protectedRoutes.push(`${method}#${hapiPath}`)
410
- }
411
-
412
- const { host, httpPort, httpsProtocol } = this.#options
413
- const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${httpPort}`
414
-
415
- this.#terminalInfo.push({
416
- invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
417
- method,
418
- path: hapiPath,
419
- server,
420
- stage:
421
- endpoint.isHttpApi || this.#options.noPrependStageInUrl ? null : stage,
422
- })
423
-
424
- const authStrategyName = this.#setAuthorizationStrategy(
373
+ #createHapiHandler(params) {
374
+ const {
375
+ additionalRequestContext,
425
376
  endpoint,
426
377
  functionKey,
378
+ hapiMethod,
379
+ hapiPath,
427
380
  method,
428
- path,
429
- )
430
-
431
- let cors = null
432
- if (endpoint.cors) {
433
- cors = {
434
- credentials:
435
- endpoint.cors.credentials || this.#options.corsConfig.credentials,
436
- exposedHeaders: this.#options.corsConfig.exposedHeaders,
437
- headers: endpoint.cors.headers || this.#options.corsConfig.headers,
438
- origin: endpoint.cors.origins || this.#options.corsConfig.origin,
439
- }
440
- } else if (
441
- this.#serverless.service.provider.httpApi &&
442
- this.#serverless.service.provider.httpApi.cors
443
- ) {
444
- const httpApiCors = getHttpApiCorsConfig(
445
- this.#serverless.service.provider.httpApi.cors,
446
- this,
447
- )
448
- cors = {
449
- credentials: httpApiCors.allowCredentials,
450
- exposedHeaders: httpApiCors.exposedResponseHeaders || [],
451
- headers: httpApiCors.allowedHeaders || [],
452
- maxAge: httpApiCors.maxAge,
453
- origin: httpApiCors.allowedOrigins || [],
454
- }
455
- }
456
-
457
- const hapiMethod = method === 'ANY' ? '*' : method
458
-
459
- const state = this.#options.disableCookieValidation
460
- ? {
461
- failAction: 'ignore',
462
- parse: false,
463
- }
464
- : {
465
- failAction: 'error',
466
- parse: true,
467
- }
468
-
469
- const hapiOptions = {
470
- auth: authStrategyName,
471
- cors,
472
- state,
473
- timeout: { socket: false },
474
- }
381
+ protectedRoute,
382
+ stage,
383
+ } = params
475
384
 
476
- // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
477
- // for more details, check https://github.com/dherault/serverless-offline/issues/204
478
- if (hapiMethod === 'HEAD') {
479
- log.notice(
480
- 'HEAD method event detected. Skipping HAPI server route mapping',
481
- )
482
-
483
- return
484
- }
485
-
486
- if (hapiMethod !== 'HEAD' && hapiMethod !== 'GET') {
487
- // maxBytes: Increase request size from 1MB default limit to 10MB.
488
- // Cf AWS API GW payload limits.
489
- hapiOptions.payload = {
490
- maxBytes: 1024 * 1024 * 10,
491
- parse: false,
492
- }
493
- }
494
-
495
- const additionalRequestContext = {}
496
- if (httpEvent.operationId) {
497
- additionalRequestContext.operationName = httpEvent.operationId
498
- }
499
-
500
- hapiOptions.tags = ['api']
501
-
502
- const hapiHandler = async (request, h) => {
385
+ return async (request, h) => {
503
386
  const requestPath =
504
387
  endpoint.isHttpApi || this.#options.noPrependStageInUrl
505
388
  ? request.path
506
389
  : request.path.substr(`/${stage}`.length)
507
390
 
508
- // Payload processing
391
+ // payload processing
509
392
  const encoding = detectEncoding(request)
510
393
 
511
394
  request.payload = request.payload && request.payload.toString(encoding)
512
395
  request.rawPayload = request.payload
513
396
 
514
- // Incomming request message
397
+ // incomming request message
515
398
  log.notice()
516
399
 
517
400
  log.notice()
518
401
  log.notice(`${method} ${request.path} (λ: ${functionKey})`)
519
402
 
520
- // Check for APIKey
403
+ // check for APIKey
521
404
  if (
522
- (protectedRoutes.includes(`${hapiMethod}#${hapiPath}`) ||
523
- protectedRoutes.includes(`ANY#${hapiPath}`)) &&
405
+ (protectedRoute === `${hapiMethod}#${hapiPath}` ||
406
+ protectedRoute === `ANY#${hapiPath}`) &&
524
407
  !this.#options.noAuth
525
408
  ) {
526
409
  const errorResponse = () =>
@@ -641,24 +524,18 @@ export default class HttpServer {
641
524
  event = request.payload || {}
642
525
  }
643
526
  } else if (integration === 'AWS_PROXY') {
644
- const stageVariables = this.#serverless.service.custom
645
- ? this.#serverless.service.custom.stageVariables
646
- : null
647
-
648
527
  const lambdaProxyIntegrationEvent =
649
528
  endpoint.isHttpApi && endpoint.payload === '2.0'
650
529
  ? new LambdaProxyIntegrationEventV2(
651
530
  request,
652
531
  stage,
653
532
  endpoint.routeKey,
654
- stageVariables,
655
533
  additionalRequestContext,
656
534
  )
657
535
  : new LambdaProxyIntegrationEvent(
658
536
  request,
659
537
  stage,
660
538
  requestPath,
661
- stageVariables,
662
539
  endpoint.isHttpApi ? endpoint.routeKey : null,
663
540
  additionalRequestContext,
664
541
  )
@@ -710,8 +587,7 @@ export default class HttpServer {
710
587
 
711
588
  const errorMessage = (err.message || err).toString()
712
589
 
713
- const re = /\[(\d{3})]/
714
- const found = errorMessage.match(re)
590
+ const found = errorMessage.match(/\[(\d{3})]/)
715
591
 
716
592
  if (found && found.length > 1) {
717
593
  ;[, errorStatusCode] = found
@@ -862,7 +738,9 @@ export default class HttpServer {
862
738
  ).getContext()
863
739
 
864
740
  result = renderVelocityTemplateObject(
865
- { root: responseTemplate },
741
+ {
742
+ root: responseTemplate,
743
+ },
866
744
  reponseContext,
867
745
  ).root
868
746
  } catch (error) {
@@ -971,7 +849,9 @@ export default class HttpServer {
971
849
  headerValue.forEach((value) => {
972
850
  // it looks like Hapi doesn't support multiple headers with the same name,
973
851
  // appending values is the closest we can come to the AWS behavior.
974
- response.header(headerKey, value, { append: true })
852
+ response.header(headerKey, value, {
853
+ append: true,
854
+ })
975
855
  })
976
856
  }
977
857
  })
@@ -1009,7 +889,6 @@ export default class HttpServer {
1009
889
  }
1010
890
  }
1011
891
 
1012
- // Log response
1013
892
  let whatToLog = result
1014
893
 
1015
894
  try {
@@ -1024,9 +903,152 @@ export default class HttpServer {
1024
903
  }
1025
904
  }
1026
905
 
1027
- // Bon voyage!
1028
906
  return response
1029
907
  }
908
+ }
909
+
910
+ createRoutes(functionKey, httpEvent, handler) {
911
+ const [handlerPath] = splitHandlerPathAndName(handler)
912
+
913
+ let method
914
+ let path
915
+ let hapiPath
916
+
917
+ if (httpEvent.isHttpApi) {
918
+ if (httpEvent.routeKey === '$default') {
919
+ method = 'ANY'
920
+ path = httpEvent.routeKey
921
+ hapiPath = '/{default*}'
922
+ } else {
923
+ ;[method, path] = httpEvent.routeKey.split(' ')
924
+ hapiPath = generateHapiPath(
925
+ path,
926
+ {
927
+ ...this.#options,
928
+ noPrependStageInUrl: true, // Serverless always uses the $default stage
929
+ },
930
+ this.#serverless,
931
+ )
932
+ }
933
+ } else {
934
+ method = httpEvent.method.toUpperCase()
935
+ ;({ path } = httpEvent)
936
+ hapiPath = generateHapiPath(path, this.#options, this.#serverless)
937
+ }
938
+
939
+ const endpoint = new Endpoint(
940
+ join(this.#serverless.config.servicePath, handlerPath),
941
+ httpEvent,
942
+ ).generate()
943
+
944
+ const stage = endpoint.isHttpApi
945
+ ? '$default'
946
+ : this.#options.stage || this.#serverless.service.provider.stage
947
+
948
+ const protectedRoute = httpEvent.private
949
+ ? `${method}#${hapiPath}`
950
+ : undefined
951
+
952
+ const { host, httpPort, httpsProtocol } = this.#options
953
+ const server = `${httpsProtocol ? 'https' : 'http'}://${host}:${httpPort}`
954
+
955
+ this.#terminalInfo.push({
956
+ invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
957
+ method,
958
+ path: hapiPath,
959
+ server,
960
+ stage:
961
+ endpoint.isHttpApi || this.#options.noPrependStageInUrl ? null : stage,
962
+ })
963
+
964
+ const authStrategyName = this.#setAuthorizationStrategy(
965
+ endpoint,
966
+ functionKey,
967
+ method,
968
+ path,
969
+ )
970
+
971
+ let cors = null
972
+ if (endpoint.cors) {
973
+ cors = {
974
+ credentials:
975
+ endpoint.cors.credentials || this.#options.corsConfig.credentials,
976
+ exposedHeaders: this.#options.corsConfig.exposedHeaders,
977
+ headers: endpoint.cors.headers || this.#options.corsConfig.headers,
978
+ origin: endpoint.cors.origins || this.#options.corsConfig.origin,
979
+ }
980
+ } else if (
981
+ this.#serverless.service.provider.httpApi &&
982
+ this.#serverless.service.provider.httpApi.cors
983
+ ) {
984
+ const httpApiCors = getHttpApiCorsConfig(
985
+ this.#serverless.service.provider.httpApi.cors,
986
+ this,
987
+ )
988
+ cors = {
989
+ credentials: httpApiCors.allowCredentials,
990
+ exposedHeaders: httpApiCors.exposedResponseHeaders || [],
991
+ headers: httpApiCors.allowedHeaders || [],
992
+ maxAge: httpApiCors.maxAge,
993
+ origin: httpApiCors.allowedOrigins || [],
994
+ }
995
+ }
996
+
997
+ const hapiMethod = method === 'ANY' ? '*' : method
998
+
999
+ const state = this.#options.disableCookieValidation
1000
+ ? {
1001
+ failAction: 'ignore',
1002
+ parse: false,
1003
+ }
1004
+ : {
1005
+ failAction: 'error',
1006
+ parse: true,
1007
+ }
1008
+
1009
+ const hapiOptions = {
1010
+ auth: authStrategyName,
1011
+ cors,
1012
+ state,
1013
+ timeout: { socket: false },
1014
+ }
1015
+
1016
+ // skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
1017
+ // for more details, check https://github.com/dherault/serverless-offline/issues/204
1018
+ if (hapiMethod === 'HEAD') {
1019
+ log.notice(
1020
+ 'HEAD method event detected. Skipping HAPI server route mapping',
1021
+ )
1022
+
1023
+ return
1024
+ }
1025
+
1026
+ if (hapiMethod !== 'HEAD' && hapiMethod !== 'GET') {
1027
+ // maxBytes: Increase request size from 1MB default limit to 10MB.
1028
+ // Cf AWS API GW payload limits.
1029
+ hapiOptions.payload = {
1030
+ maxBytes: 1024 * 1024 * 10,
1031
+ parse: false,
1032
+ }
1033
+ }
1034
+
1035
+ const additionalRequestContext = {}
1036
+ if (httpEvent.operationId) {
1037
+ additionalRequestContext.operationName = httpEvent.operationId
1038
+ }
1039
+
1040
+ hapiOptions.tags = ['api']
1041
+
1042
+ const hapiHandler = this.#createHapiHandler({
1043
+ additionalRequestContext,
1044
+ endpoint,
1045
+ functionKey,
1046
+ hapiMethod,
1047
+ hapiPath,
1048
+ method,
1049
+ protectedRoute,
1050
+ stage,
1051
+ })
1030
1052
 
1031
1053
  this.#server.route({
1032
1054
  handler: hapiHandler,
@@ -30,22 +30,12 @@ export default class LambdaProxyIntegrationEvent {
30
30
 
31
31
  #stage = null
32
32
 
33
- #stageVariables = null
34
-
35
- constructor(
36
- request,
37
- stage,
38
- path,
39
- stageVariables,
40
- routeKey,
41
- additionalRequestContext,
42
- ) {
33
+ constructor(request, stage, path, routeKey, additionalRequestContext) {
43
34
  this.#additionalRequestContext = additionalRequestContext || {}
44
35
  this.#path = path
45
36
  this.#routeKey = routeKey
46
37
  this.#request = request
47
38
  this.#stage = stage
48
- this.#stageVariables = stageVariables
49
39
  }
50
40
 
51
41
  create() {
@@ -227,7 +217,7 @@ export default class LambdaProxyIntegrationEvent {
227
217
  stage: this.#stage,
228
218
  },
229
219
  resource,
230
- stageVariables: this.#stageVariables,
220
+ stageVariables: null,
231
221
  }
232
222
  }
233
223
  }
@@ -24,20 +24,11 @@ export default class LambdaProxyIntegrationEventV2 {
24
24
 
25
25
  #stage = null
26
26
 
27
- #stageVariables = null
28
-
29
- constructor(
30
- request,
31
- stage,
32
- routeKey,
33
- stageVariables,
34
- additionalRequestContext,
35
- ) {
27
+ constructor(request, stage, routeKey, additionalRequestContext) {
36
28
  this.#additionalRequestContext = additionalRequestContext || {}
37
29
  this.#routeKey = routeKey
38
30
  this.#request = request
39
31
  this.#stage = stage
40
- this.#stageVariables = stageVariables
41
32
  }
42
33
 
43
34
  create() {
@@ -183,7 +174,7 @@ export default class LambdaProxyIntegrationEventV2 {
183
174
  timeEpoch: requestTimeEpoch,
184
175
  },
185
176
  routeKey: this.#routeKey,
186
- stageVariables: this.#stageVariables,
177
+ stageVariables: null,
187
178
  version: '2.0',
188
179
  }
189
180
  }
@@ -39,9 +39,9 @@ export default class Schedule {
39
39
  const cron = this.#convertExpressionToCron(entry)
40
40
 
41
41
  log.notice(
42
- `Scheduling [${functionKey}] cron: [${cron}] input: ${stringify(
43
- input,
44
- )}`,
42
+ `Scheduling [${functionKey}] cron: [${cron}]${
43
+ input ? ` input: ${stringify(input)}` : ''
44
+ }`,
45
45
  )
46
46
 
47
47
  nodeSchedule.scheduleJob(cron, async () => {
@@ -15,8 +15,8 @@ import {
15
15
  } from '../config/index.js'
16
16
  import { createUniqueId, splitHandlerPathAndName } from '../utils/index.js'
17
17
 
18
- const { entries, fromEntries } = Object
19
18
  const { ceil } = Math
19
+ const { entries, fromEntries } = Object
20
20
 
21
21
  export default class LambdaFunction {
22
22
  #artifact = null
@@ -41,14 +41,18 @@ export default class ChildProcessRunner {
41
41
  },
42
42
  )
43
43
 
44
- const message = new Promise((res, rej) => {
45
- childProcess.on('message', (data) => {
46
- if (data.error) rej(data.error)
47
- else res(data)
44
+ let message
45
+
46
+ try {
47
+ message = new Promise((res, rej) => {
48
+ childProcess.on('message', (data) => {
49
+ if (data.error) rej(data.error)
50
+ else res(data)
51
+ })
48
52
  })
49
- }).finally(() => {
53
+ } finally {
50
54
  childProcess.kill()
51
- })
55
+ }
52
56
 
53
57
  childProcess.send({
54
58
  context,
@@ -12,6 +12,8 @@ import pRetry from 'p-retry'
12
12
  import DockerImage from './DockerImage.js'
13
13
 
14
14
  const { stringify } = JSON
15
+ const { floor, log: mathLog } = Math
16
+ const { parseFloat } = Number
15
17
  const { entries, hasOwn } = Object
16
18
 
17
19
  export default class DockerContainer {
@@ -402,7 +404,7 @@ export default class DockerContainer {
402
404
  const dm = decimals < 0 ? 0 : decimals
403
405
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
404
406
 
405
- const i = Math.floor(Math.log(bytes) / Math.log(k))
407
+ const i = floor(mathLog(bytes) / mathLog(k))
406
408
 
407
409
  return `${parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`
408
410
  }
@@ -1,15 +1,15 @@
1
1
  import { mkdir, readFile, rm, rmdir, writeFile } from 'node:fs/promises'
2
2
  import { EOL } from 'node:os'
3
- import { sep, resolve, parse as pathParse } from 'node:path'
4
3
  import process, { chdir, cwd } from 'node:process'
4
+ import { parse as pathParse, resolve, sep } from 'node:path'
5
5
  import { log } from '@serverless/utils/log.js'
6
- import { execa, execaSync } from 'execa'
6
+ import { execa } from 'execa'
7
7
 
8
8
  const { parse, stringify } = JSON
9
9
 
10
- const PAYLOAD_IDENTIFIER = 'offline_payload'
11
-
12
10
  export default class GoRunner {
11
+ static #payloadIdentifier = 'offline_payload'
12
+
13
13
  #codeDir = null
14
14
 
15
15
  #env = null
@@ -18,10 +18,10 @@ export default class GoRunner {
18
18
 
19
19
  #handlerPath = null
20
20
 
21
- #tmpPath = null
22
-
23
21
  #tmpFile = null
24
22
 
23
+ #tmpPath = null
24
+
25
25
  constructor(funOptions, env) {
26
26
  const { handlerPath, codeDir } = funOptions
27
27
 
@@ -34,8 +34,10 @@ export default class GoRunner {
34
34
  try {
35
35
  // refresh go.mod
36
36
  await rm(this.#tmpFile)
37
- execaSync('go', ['mod', 'tidy'])
38
- await rmdir(this.#tmpPath, { recursive: true })
37
+ await execa('go', ['mod', 'tidy'])
38
+ await rmdir(this.#tmpPath, {
39
+ recursive: true,
40
+ })
39
41
  } catch {
40
42
  // @ignore
41
43
  }
@@ -49,12 +51,10 @@ export default class GoRunner {
49
51
  let payload
50
52
 
51
53
  for (const item of value.split(EOL)) {
52
- if (item.indexOf(PAYLOAD_IDENTIFIER) === -1) {
53
- logs.push(item)
54
- } else if (item.indexOf(PAYLOAD_IDENTIFIER) !== -1) {
54
+ if (item.includes(GoRunner.#payloadIdentifier)) {
55
55
  try {
56
56
  const {
57
- offline_payload: { success, error },
57
+ [GoRunner.#payloadIdentifier]: { error, success },
58
58
  } = parse(item)
59
59
 
60
60
  if (success) {
@@ -65,6 +65,8 @@ export default class GoRunner {
65
65
  } catch {
66
66
  // @ignore
67
67
  }
68
+ } else {
69
+ logs.push(item)
68
70
  }
69
71
  }
70
72
 
@@ -122,8 +124,11 @@ export default class GoRunner {
122
124
  chdir(cwdPath.substring(0, cwdPath.indexOf('main.go')))
123
125
 
124
126
  // Make sure we have the mock-lambda runner
125
- execaSync('go', ['get', 'github.com/icarus-sullivan/mock-lambda@e065469'])
126
- execaSync('go', ['build'])
127
+ await execa('go', [
128
+ 'get',
129
+ 'github.com/icarus-sullivan/mock-lambda@e065469',
130
+ ])
131
+ await execa('go', ['build'])
127
132
  } catch {
128
133
  // @ignore
129
134
  }
@@ -3,6 +3,7 @@ import { performance } from 'node:perf_hooks'
3
3
  import process from 'node:process'
4
4
  import { log } from '@serverless/utils/log.js'
5
5
 
6
+ const { floor } = Math
6
7
  const { assign } = Object
7
8
 
8
9
  const require = createRequire(import.meta.url)
@@ -49,7 +50,6 @@ export default class InProcessRunner {
49
50
  let handler
50
51
 
51
52
  try {
52
- // const { [this.#handlerName]: handler } = await import(this.#handlerPath)
53
53
  // eslint-disable-next-line import/no-dynamic-require
54
54
  ;({ [this.#handlerName]: handler } = require(this.#handlerPath))
55
55
  } catch (err) {
@@ -86,15 +86,21 @@ export default class InProcessRunner {
86
86
  // create new immutable object
87
87
  const lambdaContext = {
88
88
  ...context,
89
- done: (err, data) => callback(err, data),
90
- fail: (err) => callback(err),
91
- getRemainingTimeInMillis: () => {
89
+ done(err, data) {
90
+ callback(err, data)
91
+ },
92
+ fail(err) {
93
+ callback(err)
94
+ },
95
+ getRemainingTimeInMillis() {
92
96
  const timeLeft = executionTimeout - performance.now()
93
97
 
94
98
  // just return 0 for now if we are beyond alotted time (timeout)
95
- return timeLeft > 0 ? timeLeft : 0
99
+ return timeLeft > 0 ? floor(timeLeft) : 0
100
+ },
101
+ succeed(res) {
102
+ callback(null, res)
96
103
  },
97
- succeed: (res) => callback(null, res),
98
104
  }
99
105
 
100
106
  let result
@@ -4,9 +4,11 @@ import { log } from '@serverless/utils/log.js'
4
4
  import { invokeJavaLocal } from 'java-invoke-local'
5
5
 
6
6
  const { parse, stringify } = JSON
7
- const { has } = Reflect
7
+ const { hasOwn } = Object
8
8
 
9
9
  export default class JavaRunner {
10
+ static #payloadIdentifier = '__offline_payload__'
11
+
10
12
  #deployPackage = null
11
13
 
12
14
  #env = null
@@ -45,9 +47,9 @@ export default class JavaRunner {
45
47
  if (
46
48
  json &&
47
49
  typeof json === 'object' &&
48
- has(json, '__offline_payload__')
50
+ hasOwn(json, JavaRunner.#payloadIdentifier)
49
51
  ) {
50
- return json.__offline_payload__
52
+ return json[JavaRunner.#payloadIdentifier]
51
53
  }
52
54
  }
53
55
 
@@ -7,12 +7,13 @@ import { fileURLToPath } from 'node:url'
7
7
  import { log } from '@serverless/utils/log.js'
8
8
 
9
9
  const { parse, stringify } = JSON
10
- const { assign } = Object
11
- const { has } = Reflect
10
+ const { assign, hasOwn } = Object
12
11
 
13
12
  const __dirname = dirname(fileURLToPath(import.meta.url))
14
13
 
15
14
  export default class PythonRunner {
15
+ static #payloadIdentifier = '__offline_payload__'
16
+
16
17
  #env = null
17
18
 
18
19
  #handlerName = null
@@ -83,9 +84,9 @@ export default class PythonRunner {
83
84
  if (
84
85
  json &&
85
86
  typeof json === 'object' &&
86
- has(json, '__offline_payload__')
87
+ hasOwn(json, PythonRunner.#payloadIdentifier)
87
88
  ) {
88
- payload = json.__offline_payload__
89
+ payload = json[PythonRunner.#payloadIdentifier]
89
90
  // everything else is print(), logging, ...
90
91
  } else {
91
92
  log.notice(item)
@@ -6,11 +6,13 @@ import { log } from '@serverless/utils/log.js'
6
6
  import { execa } from 'execa'
7
7
 
8
8
  const { parse, stringify } = JSON
9
- const { has } = Reflect
9
+ const { hasOwn } = Object
10
10
 
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url))
12
12
 
13
13
  export default class RubyRunner {
14
+ static #payloadIdentifier = '__offline_payload__'
15
+
14
16
  #env = null
15
17
 
16
18
  #handlerName = null
@@ -47,9 +49,9 @@ export default class RubyRunner {
47
49
  if (
48
50
  json &&
49
51
  typeof json === 'object' &&
50
- has(json, '__offline_payload__')
52
+ hasOwn(json, RubyRunner.#payloadIdentifier)
51
53
  ) {
52
- payload = json.__offline_payload__
54
+ payload = json[RubyRunner.#payloadIdentifier]
53
55
  } else {
54
56
  log.notice(item)
55
57
  }
@@ -2,10 +2,10 @@ import { env } from 'node:process'
2
2
  import { parentPort, workerData } from 'node:worker_threads'
3
3
  import InProcessRunner from '../in-process-runner/index.js'
4
4
 
5
- const { functionKey, handlerName, handlerPath } = workerData
5
+ const { functionKey, handlerName, handlerPath, timeout } = workerData
6
6
 
7
7
  parentPort.on('message', async (messageData) => {
8
- const { context, event, port, timeout } = messageData
8
+ const { context, event, port } = messageData
9
9
 
10
10
  // TODO we could probably cache this in the module scope?
11
11
  const inProcessRunner = new InProcessRunner(