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 +8 -8
- package/src/ServerlessOffline.js +41 -3
- package/src/config/commandOptions.js +4 -0
- package/src/config/defaultOptions.js +1 -0
- package/src/events/alb/Alb.js +56 -0
- package/src/events/alb/AlbEventDefinition.js +22 -0
- package/src/events/alb/HttpServer.js +402 -0
- package/src/events/alb/index.js +1 -0
- package/src/events/alb/lambda-events/LambdaAlbRequestEvent.js +52 -0
- package/src/events/alb/lambda-events/index.js +1 -0
- package/src/events/http/HttpServer.js +2 -0
- package/src/lambda/LambdaFunction.js +1 -1
- package/src/lambda/handler-runner/python-runner/PythonRunner.js +5 -6
- package/src/utils/generateHapiPath.js +29 -0
- package/src/utils/index.js +1 -1
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": "
|
|
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.
|
|
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.
|
|
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": "^
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
124
|
+
"serverless": "^3.25.1",
|
|
125
125
|
"standard-version": "^9.5.0"
|
|
126
126
|
},
|
|
127
127
|
"peerDependencies": {
|
package/src/ServerlessOffline.js
CHANGED
|
@@ -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({
|
|
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,
|
|
@@ -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'
|
|
@@ -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((
|
|
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
|
-
|
|
120
|
+
res(parsed)
|
|
121
121
|
}
|
|
122
|
-
return null
|
|
123
122
|
} catch (err) {
|
|
124
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/utils/index.js
CHANGED
|
@@ -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
|