serverless-offline 13.3.2 → 13.3.4
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 +17 -17
- package/package.json +19 -17
- package/src/ServerlessOffline.js +42 -42
- package/src/config/colors.js +9 -9
- package/src/config/commandOptions.js +61 -61
- package/src/config/constants.js +5 -5
- package/src/config/defaultOptions.js +6 -6
- package/src/config/index.js +4 -4
- package/src/config/supportedRuntimes.js +18 -18
- package/src/errors/index.js +1 -1
- package/src/events/alb/Alb.js +2 -2
- package/src/events/alb/AlbEventDefinition.js +2 -2
- package/src/events/alb/HttpServer.js +54 -54
- package/src/events/alb/index.js +1 -1
- package/src/events/alb/lambda-events/LambdaAlbRequestEvent.js +2 -2
- package/src/events/alb/lambda-events/index.js +1 -1
- package/src/events/authCanExecuteResource.js +3 -3
- package/src/events/authFunctionNameExtractor.js +9 -9
- package/src/events/authMatchPolicyResource.js +8 -8
- package/src/events/authValidateContext.js +11 -11
- package/src/events/http/Endpoint.js +26 -26
- package/src/events/http/Http.js +2 -2
- package/src/events/http/HttpEventDefinition.js +2 -2
- package/src/events/http/HttpServer.js +157 -158
- package/src/events/http/OfflineEndpoint.js +7 -7
- package/src/events/http/authJWTSettingsExtractor.js +2 -2
- package/src/events/http/createAuthScheme.js +29 -27
- package/src/events/http/createJWTAuthScheme.js +12 -12
- package/src/events/http/index.js +1 -1
- package/src/events/http/javaHelpers.js +2 -2
- package/src/events/http/lambda-events/LambdaIntegrationEvent.js +5 -5
- package/src/events/http/lambda-events/LambdaProxyIntegrationEvent.js +47 -47
- package/src/events/http/lambda-events/LambdaProxyIntegrationEventV2.js +27 -27
- package/src/events/http/lambda-events/VelocityContext.js +28 -32
- package/src/events/http/lambda-events/index.js +4 -4
- package/src/events/http/lambda-events/renderVelocityTemplateObject.js +13 -13
- package/src/events/http/parseResources.js +6 -6
- package/src/events/http/payloadSchemaValidator.js +2 -2
- package/src/events/schedule/Schedule.js +21 -21
- package/src/events/schedule/ScheduleEvent.js +7 -7
- package/src/events/schedule/ScheduleEventDefinition.js +1 -1
- package/src/events/schedule/index.js +1 -1
- package/src/events/websocket/HttpServer.js +9 -9
- package/src/events/websocket/WebSocket.js +4 -4
- package/src/events/websocket/WebSocketClients.js +29 -29
- package/src/events/websocket/WebSocketEventDefinition.js +1 -1
- package/src/events/websocket/WebSocketServer.js +9 -9
- package/src/events/websocket/http-routes/_catchAll/catchAllRoute.js +3 -3
- package/src/events/websocket/http-routes/_catchAll/index.js +1 -1
- package/src/events/websocket/http-routes/connections/ConnectionsController.js +1 -1
- package/src/events/websocket/http-routes/connections/connectionsRoutes.js +6 -6
- package/src/events/websocket/http-routes/connections/index.js +1 -1
- package/src/events/websocket/http-routes/index.js +2 -2
- package/src/events/websocket/index.js +1 -1
- package/src/events/websocket/lambda-events/WebSocketAuthorizerEvent.js +5 -5
- package/src/events/websocket/lambda-events/WebSocketConnectEvent.js +7 -6
- package/src/events/websocket/lambda-events/WebSocketDisconnectEvent.js +5 -5
- package/src/events/websocket/lambda-events/WebSocketEvent.js +2 -2
- package/src/events/websocket/lambda-events/WebSocketRequestContext.js +12 -11
- package/src/events/websocket/lambda-events/index.js +4 -4
- package/src/index.js +1 -1
- package/src/lambda/HttpServer.js +11 -11
- package/src/lambda/Lambda.js +2 -2
- package/src/lambda/LambdaContext.js +1 -1
- package/src/lambda/LambdaFunction.js +34 -34
- package/src/lambda/LambdaFunctionPool.js +3 -3
- package/src/lambda/handler-runner/HandlerRunner.js +12 -12
- package/src/lambda/handler-runner/docker-runner/DockerContainer.js +59 -59
- package/src/lambda/handler-runner/docker-runner/DockerImage.js +6 -6
- package/src/lambda/handler-runner/docker-runner/DockerRunner.js +2 -2
- package/src/lambda/handler-runner/docker-runner/index.js +1 -1
- package/src/lambda/handler-runner/go-runner/GoRunner.js +34 -34
- package/src/lambda/handler-runner/go-runner/index.js +1 -1
- package/src/lambda/handler-runner/in-process-runner/InProcessRunner.js +7 -7
- package/src/lambda/handler-runner/in-process-runner/aws-lambda-ric/UserFunction.js +52 -52
- package/src/lambda/handler-runner/in-process-runner/index.js +1 -1
- package/src/lambda/handler-runner/index.js +1 -1
- package/src/lambda/handler-runner/java-runner/JavaRunner.js +13 -13
- package/src/lambda/handler-runner/java-runner/index.js +1 -1
- package/src/lambda/handler-runner/python-runner/PythonRunner.js +21 -21
- package/src/lambda/handler-runner/python-runner/index.js +1 -1
- package/src/lambda/handler-runner/ruby-runner/RubyRunner.js +11 -11
- package/src/lambda/handler-runner/ruby-runner/index.js +1 -1
- package/src/lambda/handler-runner/worker-thread-runner/WorkerThreadRunner.js +6 -6
- package/src/lambda/handler-runner/worker-thread-runner/index.js +1 -1
- package/src/lambda/handler-runner/worker-thread-runner/workerThreadHelper.js +4 -4
- package/src/lambda/index.js +1 -1
- package/src/lambda/routes/index.js +2 -2
- package/src/lambda/routes/invocations/InvocationsController.js +11 -11
- package/src/lambda/routes/invocations/index.js +1 -1
- package/src/lambda/routes/invocations/invocationsRoute.js +15 -15
- package/src/lambda/routes/invoke-async/index.js +1 -1
- package/src/lambda/routes/invoke-async/invokeAsyncRoute.js +6 -6
- package/src/utils/checkDockerDaemon.js +8 -8
- package/src/utils/checkGoVersion.js +3 -3
- package/src/utils/createApiKey.js +2 -2
- package/src/utils/detectExecutable.js +2 -2
- package/src/utils/formatToClfTime.js +2 -2
- package/src/utils/generateHapiPath.js +8 -8
- package/src/utils/getApiKeysValues.js +2 -2
- package/src/utils/getHttpApiCorsConfig.js +11 -11
- package/src/utils/getRawQueryParams.js +2 -2
- package/src/utils/index.js +25 -26
- package/src/utils/jsonPath.js +1 -1
- package/src/utils/logRoutes.js +13 -13
- package/src/utils/parseHeaders.js +1 -1
- package/src/utils/parseMultiValueHeaders.js +1 -1
- package/src/utils/parseMultiValueQueryStringParameters.js +1 -1
- package/src/utils/parseQueryStringParameters.js +1 -1
- package/src/utils/parseQueryStringParametersForPayloadV2.js +1 -1
- package/src/utils/splitHandlerPathAndName.js +3 -3
- package/src/utils/createUniqueId.js +0 -5
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { Buffer } from
|
|
2
|
-
import { readFile } from
|
|
3
|
-
import { createRequire } from
|
|
4
|
-
import { join, resolve } from
|
|
5
|
-
import { exit } from
|
|
6
|
-
import h2o2 from
|
|
7
|
-
import { Server } from
|
|
8
|
-
import { log } from
|
|
9
|
-
import authFunctionNameExtractor from
|
|
10
|
-
import authJWTSettingsExtractor from
|
|
11
|
-
import createAuthScheme from
|
|
12
|
-
import createJWTAuthScheme from
|
|
13
|
-
import Endpoint from
|
|
1
|
+
import { Buffer } from "node:buffer"
|
|
2
|
+
import { readFile } from "node:fs/promises"
|
|
3
|
+
import { createRequire } from "node:module"
|
|
4
|
+
import { join, resolve } from "node:path"
|
|
5
|
+
import { exit } from "node:process"
|
|
6
|
+
import h2o2 from "@hapi/h2o2"
|
|
7
|
+
import { Server } from "@hapi/hapi"
|
|
8
|
+
import { log } from "@serverless/utils/log.js"
|
|
9
|
+
import authFunctionNameExtractor from "../authFunctionNameExtractor.js"
|
|
10
|
+
import authJWTSettingsExtractor from "./authJWTSettingsExtractor.js"
|
|
11
|
+
import createAuthScheme from "./createAuthScheme.js"
|
|
12
|
+
import createJWTAuthScheme from "./createJWTAuthScheme.js"
|
|
13
|
+
import Endpoint from "./Endpoint.js"
|
|
14
14
|
import {
|
|
15
15
|
LambdaIntegrationEvent,
|
|
16
16
|
LambdaProxyIntegrationEvent,
|
|
17
17
|
renderVelocityTemplateObject,
|
|
18
18
|
VelocityContext,
|
|
19
|
-
} from
|
|
20
|
-
import LambdaProxyIntegrationEventV2 from
|
|
21
|
-
import parseResources from
|
|
22
|
-
import payloadSchemaValidator from
|
|
23
|
-
import logRoutes from
|
|
19
|
+
} from "./lambda-events/index.js"
|
|
20
|
+
import LambdaProxyIntegrationEventV2 from "./lambda-events/LambdaProxyIntegrationEventV2.js"
|
|
21
|
+
import parseResources from "./parseResources.js"
|
|
22
|
+
import payloadSchemaValidator from "./payloadSchemaValidator.js"
|
|
23
|
+
import logRoutes from "../../utils/logRoutes.js"
|
|
24
24
|
import {
|
|
25
25
|
createApiKey,
|
|
26
26
|
detectEncoding,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
getHttpApiCorsConfig,
|
|
30
30
|
jsonPath,
|
|
31
31
|
splitHandlerPathAndName,
|
|
32
|
-
} from
|
|
32
|
+
} from "../../utils/index.js"
|
|
33
33
|
|
|
34
34
|
const { parse, stringify } = JSON
|
|
35
35
|
const { assign, entries, keys } = Object
|
|
@@ -57,8 +57,8 @@ export default class HttpServer {
|
|
|
57
57
|
|
|
58
58
|
async #loadCerts(httpsProtocol) {
|
|
59
59
|
const [cert, key] = await Promise.all([
|
|
60
|
-
readFile(resolve(httpsProtocol,
|
|
61
|
-
readFile(resolve(httpsProtocol,
|
|
60
|
+
readFile(resolve(httpsProtocol, "cert.pem"), "utf8"),
|
|
61
|
+
readFile(resolve(httpsProtocol, "key.pem"), "utf8"),
|
|
62
62
|
])
|
|
63
63
|
|
|
64
64
|
return {
|
|
@@ -104,7 +104,7 @@ export default class HttpServer {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
// Enable CORS preflight response
|
|
107
|
-
this.#server.ext(
|
|
107
|
+
this.#server.ext("onPreResponse", (request, h) => {
|
|
108
108
|
if (request.headers.origin) {
|
|
109
109
|
const response = request.response.isBoom
|
|
110
110
|
? request.response.output
|
|
@@ -123,11 +123,11 @@ export default class HttpServer {
|
|
|
123
123
|
this,
|
|
124
124
|
)
|
|
125
125
|
|
|
126
|
-
if (request.method ===
|
|
126
|
+
if (request.method === "options") {
|
|
127
127
|
response.statusCode = 204
|
|
128
128
|
const allowAllOrigins =
|
|
129
129
|
httpApiCors.allowedOrigins.length === 1 &&
|
|
130
|
-
httpApiCors.allowedOrigins[0] ===
|
|
130
|
+
httpApiCors.allowedOrigins[0] === "*"
|
|
131
131
|
if (
|
|
132
132
|
!allowAllOrigins &&
|
|
133
133
|
!httpApiCors.allowedOrigins.includes(request.headers.origin)
|
|
@@ -136,47 +136,47 @@ export default class HttpServer {
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
response.headers[
|
|
139
|
+
response.headers["access-control-allow-origin"] =
|
|
140
140
|
request.headers.origin
|
|
141
141
|
if (httpApiCors.allowCredentials) {
|
|
142
|
-
response.headers[
|
|
142
|
+
response.headers["access-control-allow-credentials"] = "true"
|
|
143
143
|
}
|
|
144
144
|
if (httpApiCors.maxAge) {
|
|
145
|
-
response.headers[
|
|
145
|
+
response.headers["access-control-max-age"] = httpApiCors.maxAge
|
|
146
146
|
}
|
|
147
147
|
if (httpApiCors.exposedResponseHeaders) {
|
|
148
|
-
response.headers[
|
|
149
|
-
httpApiCors.exposedResponseHeaders.join(
|
|
148
|
+
response.headers["access-control-expose-headers"] =
|
|
149
|
+
httpApiCors.exposedResponseHeaders.join(",")
|
|
150
150
|
}
|
|
151
151
|
if (httpApiCors.allowedMethods) {
|
|
152
|
-
response.headers[
|
|
153
|
-
httpApiCors.allowedMethods.join(
|
|
152
|
+
response.headers["access-control-allow-methods"] =
|
|
153
|
+
httpApiCors.allowedMethods.join(",")
|
|
154
154
|
}
|
|
155
155
|
if (httpApiCors.allowedHeaders) {
|
|
156
|
-
response.headers[
|
|
157
|
-
httpApiCors.allowedHeaders.join(
|
|
156
|
+
response.headers["access-control-allow-headers"] =
|
|
157
|
+
httpApiCors.allowedHeaders.join(",")
|
|
158
158
|
}
|
|
159
159
|
} else {
|
|
160
|
-
response.headers[
|
|
160
|
+
response.headers["access-control-allow-origin"] =
|
|
161
161
|
request.headers.origin
|
|
162
|
-
response.headers[
|
|
162
|
+
response.headers["access-control-allow-credentials"] = "true"
|
|
163
163
|
|
|
164
|
-
if (request.method ===
|
|
164
|
+
if (request.method === "options") {
|
|
165
165
|
response.statusCode = 200
|
|
166
166
|
|
|
167
|
-
response.headers[
|
|
168
|
-
request.headers[
|
|
169
|
-
|
|
170
|
-
response.headers[
|
|
167
|
+
response.headers["access-control-expose-headers"] =
|
|
168
|
+
request.headers["access-control-expose-headers"] ||
|
|
169
|
+
"content-type, content-length, etag"
|
|
170
|
+
response.headers["access-control-max-age"] = 60 * 10
|
|
171
171
|
|
|
172
|
-
if (request.headers[
|
|
173
|
-
response.headers[
|
|
174
|
-
request.headers[
|
|
172
|
+
if (request.headers["access-control-request-headers"]) {
|
|
173
|
+
response.headers["access-control-allow-headers"] =
|
|
174
|
+
request.headers["access-control-request-headers"]
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
-
if (request.headers[
|
|
178
|
-
response.headers[
|
|
179
|
-
request.headers[
|
|
177
|
+
if (request.headers["access-control-request-method"]) {
|
|
178
|
+
response.headers["access-control-allow-methods"] =
|
|
179
|
+
request.headers["access-control-request-method"]
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
@@ -206,7 +206,7 @@ export default class HttpServer {
|
|
|
206
206
|
}
|
|
207
207
|
|
|
208
208
|
// TODO move the following block
|
|
209
|
-
const server = `${httpsProtocol ?
|
|
209
|
+
const server = `${httpsProtocol ? "https" : "http"}://${host}:${httpPort}`
|
|
210
210
|
|
|
211
211
|
log.notice(`Server ready: ${server} 🚀`)
|
|
212
212
|
}
|
|
@@ -220,7 +220,7 @@ export default class HttpServer {
|
|
|
220
220
|
|
|
221
221
|
#logPluginIssue() {
|
|
222
222
|
log.notice(
|
|
223
|
-
|
|
223
|
+
"If you think this is an issue with the plugin please submit it, thanks!\nhttps://github.com/dherault/serverless-offline/issues",
|
|
224
224
|
)
|
|
225
225
|
log.notice()
|
|
226
226
|
}
|
|
@@ -243,7 +243,7 @@ export default class HttpServer {
|
|
|
243
243
|
// right now _configureJWTAuthorization only handles AWS HttpAPI Gateway JWT
|
|
244
244
|
// authorizers that are defined in the serverless file
|
|
245
245
|
if (
|
|
246
|
-
this.#serverless.service.provider.name !==
|
|
246
|
+
this.#serverless.service.provider.name !== "aws" ||
|
|
247
247
|
!endpoint.isHttpApi
|
|
248
248
|
) {
|
|
249
249
|
return null
|
|
@@ -253,8 +253,8 @@ export default class HttpServer {
|
|
|
253
253
|
(endpoint.authorizer.name &&
|
|
254
254
|
this.#serverless.service.provider?.httpApi?.authorizers?.[
|
|
255
255
|
endpoint.authorizer.name
|
|
256
|
-
]?.type ===
|
|
257
|
-
endpoint.authorizer.type ===
|
|
256
|
+
]?.type === "request") ||
|
|
257
|
+
endpoint.authorizer.type === "request"
|
|
258
258
|
) {
|
|
259
259
|
return null
|
|
260
260
|
}
|
|
@@ -321,17 +321,17 @@ export default class HttpServer {
|
|
|
321
321
|
false,
|
|
322
322
|
identitySource: serverlessAuthorizerOptions?.identitySource,
|
|
323
323
|
identityValidationExpression:
|
|
324
|
-
serverlessAuthorizerOptions?.identityValidationExpression ||
|
|
324
|
+
serverlessAuthorizerOptions?.identityValidationExpression || "(.*)",
|
|
325
325
|
payloadVersion: endpoint.isHttpApi
|
|
326
|
-
? serverlessAuthorizerOptions?.payloadVersion ||
|
|
327
|
-
:
|
|
326
|
+
? serverlessAuthorizerOptions?.payloadVersion || "2.0"
|
|
327
|
+
: "1.0",
|
|
328
328
|
resultTtlInSeconds:
|
|
329
|
-
serverlessAuthorizerOptions?.resultTtlInSeconds ||
|
|
329
|
+
serverlessAuthorizerOptions?.resultTtlInSeconds || "300",
|
|
330
330
|
}
|
|
331
331
|
|
|
332
332
|
if (
|
|
333
333
|
authorizerOptions.enableSimpleResponses &&
|
|
334
|
-
authorizerOptions.payloadVersion ===
|
|
334
|
+
authorizerOptions.payloadVersion === "1.0"
|
|
335
335
|
) {
|
|
336
336
|
log.error(
|
|
337
337
|
`Cannot create Authorization function '${authFunctionName}' if payloadVersion is '1.0' and enableSimpleResponses is true`,
|
|
@@ -339,7 +339,7 @@ export default class HttpServer {
|
|
|
339
339
|
return null
|
|
340
340
|
}
|
|
341
341
|
|
|
342
|
-
if (typeof endpoint.authorizer ===
|
|
342
|
+
if (typeof endpoint.authorizer === "string") {
|
|
343
343
|
authorizerOptions.name = authFunctionName
|
|
344
344
|
} else {
|
|
345
345
|
assign(authorizerOptions, endpoint.authorizer)
|
|
@@ -348,11 +348,11 @@ export default class HttpServer {
|
|
|
348
348
|
if (
|
|
349
349
|
!authorizerOptions.identitySource &&
|
|
350
350
|
!(
|
|
351
|
-
authorizerOptions.type ===
|
|
351
|
+
authorizerOptions.type === "request" &&
|
|
352
352
|
authorizerOptions.resultTtlInSeconds === 0
|
|
353
353
|
)
|
|
354
354
|
) {
|
|
355
|
-
authorizerOptions.identitySource =
|
|
355
|
+
authorizerOptions.identitySource = "method.request.header.Authorization"
|
|
356
356
|
}
|
|
357
357
|
|
|
358
358
|
// Create a unique scheme per endpoint
|
|
@@ -391,7 +391,7 @@ export default class HttpServer {
|
|
|
391
391
|
customizations &&
|
|
392
392
|
customizations.offline?.customAuthenticationProvider
|
|
393
393
|
) {
|
|
394
|
-
const root = resolve(this.#serverless.serviceDir,
|
|
394
|
+
const root = resolve(this.#serverless.serviceDir, "require-resolver")
|
|
395
395
|
const customRequire = createRequire(root)
|
|
396
396
|
|
|
397
397
|
const provider = customRequire(
|
|
@@ -457,13 +457,13 @@ export default class HttpServer {
|
|
|
457
457
|
const errorResponse = () =>
|
|
458
458
|
h
|
|
459
459
|
.response({
|
|
460
|
-
message:
|
|
460
|
+
message: "Forbidden",
|
|
461
461
|
})
|
|
462
462
|
.code(403)
|
|
463
|
-
.header(
|
|
464
|
-
.type(
|
|
463
|
+
.header("x-amzn-ErrorType", "ForbiddenException")
|
|
464
|
+
.type("application/json")
|
|
465
465
|
|
|
466
|
-
const apiKey = request.headers[
|
|
466
|
+
const apiKey = request.headers["x-api-key"]
|
|
467
467
|
|
|
468
468
|
if (apiKey) {
|
|
469
469
|
if (!this.#apiKeysValues.has(apiKey)) {
|
|
@@ -495,26 +495,26 @@ export default class HttpServer {
|
|
|
495
495
|
}
|
|
496
496
|
|
|
497
497
|
const response = h.response()
|
|
498
|
-
const contentType = request.mime ||
|
|
498
|
+
const contentType = request.mime || "application/json" // default content type
|
|
499
499
|
|
|
500
500
|
const { integration, requestTemplates } = endpoint
|
|
501
501
|
|
|
502
502
|
// default request template to '' if we don't have a definition pushed in from serverless or endpoint
|
|
503
503
|
const requestTemplate =
|
|
504
|
-
requestTemplates !== undefined && integration ===
|
|
504
|
+
requestTemplates !== undefined && integration === "AWS"
|
|
505
505
|
? requestTemplates[contentType]
|
|
506
|
-
:
|
|
506
|
+
: ""
|
|
507
507
|
|
|
508
508
|
const schemas =
|
|
509
509
|
endpoint?.request?.schemas === undefined
|
|
510
|
-
?
|
|
510
|
+
? ""
|
|
511
511
|
: endpoint.request.schemas[contentType]
|
|
512
512
|
|
|
513
513
|
// https://hapijs.com/api#route-configuration doesn't seem to support selectively parsing
|
|
514
514
|
// so we have to do it ourselves
|
|
515
515
|
const contentTypesThatRequirePayloadParsing = [
|
|
516
|
-
|
|
517
|
-
|
|
516
|
+
"application/json",
|
|
517
|
+
"application/vnd.api+json",
|
|
518
518
|
]
|
|
519
519
|
|
|
520
520
|
if (
|
|
@@ -524,22 +524,22 @@ export default class HttpServer {
|
|
|
524
524
|
) {
|
|
525
525
|
try {
|
|
526
526
|
if (!request.payload || request.payload.length === 0) {
|
|
527
|
-
request.payload =
|
|
527
|
+
request.payload = "{}"
|
|
528
528
|
}
|
|
529
529
|
|
|
530
530
|
request.payload = parse(request.payload)
|
|
531
531
|
} catch (err) {
|
|
532
|
-
log.debug(
|
|
532
|
+
log.debug("error in converting request.payload to JSON:", err)
|
|
533
533
|
}
|
|
534
534
|
}
|
|
535
535
|
|
|
536
|
-
log.debug(
|
|
537
|
-
log.debug(
|
|
538
|
-
log.debug(
|
|
536
|
+
log.debug("contentType:", contentType)
|
|
537
|
+
log.debug("requestTemplate:", requestTemplate)
|
|
538
|
+
log.debug("payload:", request.payload)
|
|
539
539
|
|
|
540
540
|
/* REQUEST PAYLOAD SCHEMA VALIDATION */
|
|
541
541
|
if (schemas) {
|
|
542
|
-
log.debug(
|
|
542
|
+
log.debug("schemas:", schemas)
|
|
543
543
|
|
|
544
544
|
try {
|
|
545
545
|
payloadSchemaValidator(schemas, request.payload)
|
|
@@ -552,10 +552,10 @@ export default class HttpServer {
|
|
|
552
552
|
|
|
553
553
|
let event = {}
|
|
554
554
|
|
|
555
|
-
if (integration ===
|
|
555
|
+
if (integration === "AWS") {
|
|
556
556
|
if (requestTemplate) {
|
|
557
557
|
try {
|
|
558
|
-
log.debug(
|
|
558
|
+
log.debug("_____ REQUEST TEMPLATE PROCESSING _____")
|
|
559
559
|
|
|
560
560
|
event = new LambdaIntegrationEvent(
|
|
561
561
|
request,
|
|
@@ -570,12 +570,12 @@ export default class HttpServer {
|
|
|
570
570
|
err,
|
|
571
571
|
)
|
|
572
572
|
}
|
|
573
|
-
} else if (typeof request.payload ===
|
|
573
|
+
} else if (typeof request.payload === "object") {
|
|
574
574
|
event = request.payload || {}
|
|
575
575
|
}
|
|
576
|
-
} else if (integration ===
|
|
576
|
+
} else if (integration === "AWS_PROXY") {
|
|
577
577
|
const lambdaProxyIntegrationEvent =
|
|
578
|
-
endpoint.isHttpApi && endpoint.payload ===
|
|
578
|
+
endpoint.isHttpApi && endpoint.payload === "2.0"
|
|
579
579
|
? new LambdaProxyIntegrationEventV2(
|
|
580
580
|
request,
|
|
581
581
|
stage,
|
|
@@ -593,7 +593,7 @@ export default class HttpServer {
|
|
|
593
593
|
event = lambdaProxyIntegrationEvent.create()
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
-
log.debug(
|
|
596
|
+
log.debug("event:", event)
|
|
597
597
|
|
|
598
598
|
const lambdaFunction = this.#lambda.get(functionKey)
|
|
599
599
|
|
|
@@ -611,15 +611,15 @@ export default class HttpServer {
|
|
|
611
611
|
// const processResponse = (err, data) => {
|
|
612
612
|
// Everything in this block happens once the lambda function has resolved
|
|
613
613
|
|
|
614
|
-
log.debug(
|
|
614
|
+
log.debug("_____ HANDLER RESOLVED _____")
|
|
615
615
|
|
|
616
|
-
let responseName =
|
|
616
|
+
let responseName = "default"
|
|
617
617
|
const { contentHandling, responseContentType } = endpoint
|
|
618
618
|
|
|
619
619
|
/* RESPONSE SELECTION (among endpoint's possible responses) */
|
|
620
620
|
|
|
621
621
|
// Failure handling
|
|
622
|
-
let errorStatusCode =
|
|
622
|
+
let errorStatusCode = "502"
|
|
623
623
|
|
|
624
624
|
if (err) {
|
|
625
625
|
const errorMessage = (err.message || err).toString()
|
|
@@ -629,7 +629,7 @@ export default class HttpServer {
|
|
|
629
629
|
if (found && found.length > 1) {
|
|
630
630
|
;[, errorStatusCode] = found
|
|
631
631
|
} else {
|
|
632
|
-
errorStatusCode =
|
|
632
|
+
errorStatusCode = "502"
|
|
633
633
|
}
|
|
634
634
|
|
|
635
635
|
// Mocks Lambda errors
|
|
@@ -643,7 +643,7 @@ export default class HttpServer {
|
|
|
643
643
|
|
|
644
644
|
for (const [key, value] of entries(endpoint.responses)) {
|
|
645
645
|
if (
|
|
646
|
-
key !==
|
|
646
|
+
key !== "default" &&
|
|
647
647
|
`^${value.selectionPattern || key}$`.test(errorMessage)
|
|
648
648
|
) {
|
|
649
649
|
responseName = key
|
|
@@ -654,14 +654,13 @@ export default class HttpServer {
|
|
|
654
654
|
|
|
655
655
|
log.debug(`Using response '${responseName}'`)
|
|
656
656
|
|
|
657
|
-
const chosenResponse = endpoint.responses[responseName]
|
|
658
|
-
|
|
657
|
+
const chosenResponse = endpoint.responses?.[responseName] ?? {}
|
|
659
658
|
/* RESPONSE PARAMETERS PROCCESSING */
|
|
660
659
|
|
|
661
660
|
const { responseParameters } = chosenResponse
|
|
662
661
|
|
|
663
662
|
if (responseParameters) {
|
|
664
|
-
log.debug(
|
|
663
|
+
log.debug("_____ RESPONSE PARAMETERS PROCCESSING _____")
|
|
665
664
|
log.debug(
|
|
666
665
|
`Found ${
|
|
667
666
|
keys(responseParameters).length
|
|
@@ -670,27 +669,27 @@ export default class HttpServer {
|
|
|
670
669
|
|
|
671
670
|
// responseParameters use the following shape: "key": "value"
|
|
672
671
|
entries(responseParameters).forEach(([key, value]) => {
|
|
673
|
-
const keyArray = key.split(
|
|
674
|
-
const valueArray = value.split(
|
|
672
|
+
const keyArray = key.split(".") // eg: "method.response.header.location"
|
|
673
|
+
const valueArray = value.split(".") // eg: "integration.response.body.redirect.url"
|
|
675
674
|
|
|
676
675
|
log.debug(`Processing responseParameter "${key}": "${value}"`)
|
|
677
676
|
|
|
678
677
|
// For now the plugin only supports modifying headers
|
|
679
|
-
if (key.startsWith(
|
|
680
|
-
const headerName = keyArray.slice(3).join(
|
|
678
|
+
if (key.startsWith("method.response.header") && keyArray[3]) {
|
|
679
|
+
const headerName = keyArray.slice(3).join(".")
|
|
681
680
|
let headerValue
|
|
682
681
|
|
|
683
|
-
log.debug(
|
|
682
|
+
log.debug("Found header in left-hand:", headerName)
|
|
684
683
|
|
|
685
|
-
if (value.startsWith(
|
|
686
|
-
if (valueArray[2] ===
|
|
687
|
-
log.debug(
|
|
684
|
+
if (value.startsWith("integration.response")) {
|
|
685
|
+
if (valueArray[2] === "body") {
|
|
686
|
+
log.debug("Found body in right-hand")
|
|
688
687
|
|
|
689
688
|
headerValue = valueArray[3]
|
|
690
|
-
? jsonPath(result, valueArray.slice(3).join(
|
|
689
|
+
? jsonPath(result, valueArray.slice(3).join("."))
|
|
691
690
|
: result
|
|
692
691
|
|
|
693
|
-
headerValue = headerValue == null ?
|
|
692
|
+
headerValue = headerValue == null ? "" : String(headerValue)
|
|
694
693
|
} else {
|
|
695
694
|
log.notice()
|
|
696
695
|
|
|
@@ -706,7 +705,7 @@ export default class HttpServer {
|
|
|
706
705
|
headerValue = /^'.*'$/.test(value) ? value.slice(1, -1) : value // See #34
|
|
707
706
|
}
|
|
708
707
|
// Applies the header;
|
|
709
|
-
if (headerValue ===
|
|
708
|
+
if (headerValue === "") {
|
|
710
709
|
log.warning(
|
|
711
710
|
`Empty value for responseParameter "${key}": "${value}", it won't be set`,
|
|
712
711
|
)
|
|
@@ -733,13 +732,13 @@ export default class HttpServer {
|
|
|
733
732
|
|
|
734
733
|
let statusCode = 200
|
|
735
734
|
|
|
736
|
-
if (integration ===
|
|
735
|
+
if (integration === "AWS") {
|
|
737
736
|
const endpointResponseHeaders =
|
|
738
737
|
(endpoint.response && endpoint.response.headers) || {}
|
|
739
738
|
|
|
740
739
|
entries(endpointResponseHeaders)
|
|
741
740
|
.filter(
|
|
742
|
-
([, value]) => typeof value ===
|
|
741
|
+
([, value]) => typeof value === "string" && /^'.*?'$/.test(value),
|
|
743
742
|
)
|
|
744
743
|
.forEach(([key, value]) => response.header(key, value.slice(1, -1)))
|
|
745
744
|
|
|
@@ -749,14 +748,14 @@ export default class HttpServer {
|
|
|
749
748
|
const { responseTemplates } = chosenResponse
|
|
750
749
|
|
|
751
750
|
if (
|
|
752
|
-
typeof responseTemplates ===
|
|
751
|
+
typeof responseTemplates === "object" &&
|
|
753
752
|
keys(responseTemplates).length > 0
|
|
754
753
|
) {
|
|
755
754
|
// BAD IMPLEMENTATION: first key in responseTemplates
|
|
756
755
|
const responseTemplate = responseTemplates[responseContentType]
|
|
757
756
|
|
|
758
|
-
if (responseTemplate && responseTemplate !==
|
|
759
|
-
log.debug(
|
|
757
|
+
if (responseTemplate && responseTemplate !== "\n") {
|
|
758
|
+
log.debug("_____ RESPONSE TEMPLATE PROCCESSING _____")
|
|
760
759
|
log.debug(`Using responseTemplate '${responseContentType}'`)
|
|
761
760
|
|
|
762
761
|
try {
|
|
@@ -794,34 +793,34 @@ export default class HttpServer {
|
|
|
794
793
|
log.warning(`No statusCode found for response "${responseName}".`)
|
|
795
794
|
}
|
|
796
795
|
|
|
797
|
-
response.header(
|
|
796
|
+
response.header("Content-Type", responseContentType, {
|
|
798
797
|
override: false, // Maybe a responseParameter set it already. See #34
|
|
799
798
|
})
|
|
800
799
|
|
|
801
800
|
response.statusCode = statusCode
|
|
802
801
|
|
|
803
|
-
if (contentHandling ===
|
|
804
|
-
response.encoding =
|
|
805
|
-
response.source = Buffer.from(result,
|
|
806
|
-
response.variety =
|
|
807
|
-
} else if (typeof result ===
|
|
802
|
+
if (contentHandling === "CONVERT_TO_BINARY") {
|
|
803
|
+
response.encoding = "binary"
|
|
804
|
+
response.source = Buffer.from(result, "base64")
|
|
805
|
+
response.variety = "buffer"
|
|
806
|
+
} else if (typeof result === "string") {
|
|
808
807
|
response.source = stringify(result)
|
|
809
808
|
} else {
|
|
810
809
|
response.source = result
|
|
811
810
|
}
|
|
812
|
-
} else if (integration ===
|
|
811
|
+
} else if (integration === "AWS_PROXY") {
|
|
813
812
|
/* LAMBDA PROXY INTEGRATION HAPIJS RESPONSE CONFIGURATION */
|
|
814
813
|
|
|
815
814
|
if (
|
|
816
815
|
endpoint.isHttpApi &&
|
|
817
|
-
endpoint.payload ===
|
|
818
|
-
(typeof result ===
|
|
816
|
+
endpoint.payload === "2.0" &&
|
|
817
|
+
(typeof result === "string" || !result.statusCode)
|
|
819
818
|
) {
|
|
820
|
-
const body = typeof result ===
|
|
819
|
+
const body = typeof result === "string" ? result : stringify(result)
|
|
821
820
|
result = {
|
|
822
821
|
body,
|
|
823
822
|
headers: {
|
|
824
|
-
|
|
823
|
+
"Content-Type": "application/json",
|
|
825
824
|
},
|
|
826
825
|
isBase64Encoded: false,
|
|
827
826
|
statusCode: 200,
|
|
@@ -855,20 +854,20 @@ export default class HttpServer {
|
|
|
855
854
|
)
|
|
856
855
|
}
|
|
857
856
|
|
|
858
|
-
log.debug(
|
|
857
|
+
log.debug("headers", headers)
|
|
859
858
|
|
|
860
859
|
const parseCookies = (headerValue) => {
|
|
861
|
-
const cookieName = headerValue.slice(0, headerValue.indexOf(
|
|
862
|
-
const cookieValue = headerValue.slice(headerValue.indexOf(
|
|
860
|
+
const cookieName = headerValue.slice(0, headerValue.indexOf("="))
|
|
861
|
+
const cookieValue = headerValue.slice(headerValue.indexOf("=") + 1)
|
|
863
862
|
|
|
864
863
|
h.state(cookieName, cookieValue, {
|
|
865
|
-
encoding:
|
|
864
|
+
encoding: "none",
|
|
866
865
|
strictHeader: false,
|
|
867
866
|
})
|
|
868
867
|
}
|
|
869
868
|
|
|
870
869
|
entries(headers).forEach(([headerKey, headerValue]) => {
|
|
871
|
-
if (headerKey.toLowerCase() ===
|
|
870
|
+
if (headerKey.toLowerCase() === "set-cookie") {
|
|
872
871
|
headerValue.forEach(parseCookies)
|
|
873
872
|
} else {
|
|
874
873
|
headerValue.forEach((value) => {
|
|
@@ -883,30 +882,30 @@ export default class HttpServer {
|
|
|
883
882
|
|
|
884
883
|
if (
|
|
885
884
|
endpoint.isHttpApi &&
|
|
886
|
-
endpoint.payload ===
|
|
885
|
+
endpoint.payload === "2.0" &&
|
|
887
886
|
result.cookies
|
|
888
887
|
) {
|
|
889
888
|
result.cookies.forEach(parseCookies)
|
|
890
889
|
}
|
|
891
890
|
|
|
892
|
-
response.header(
|
|
891
|
+
response.header("Content-Type", "application/json", {
|
|
893
892
|
duplicate: false,
|
|
894
893
|
override: false,
|
|
895
894
|
})
|
|
896
895
|
|
|
897
|
-
if (typeof result ===
|
|
896
|
+
if (typeof result === "string") {
|
|
898
897
|
response.source = stringify(result)
|
|
899
898
|
} else if (result && result.body !== undefined) {
|
|
900
899
|
if (result.isBase64Encoded) {
|
|
901
|
-
response.encoding =
|
|
902
|
-
response.source = Buffer.from(result.body,
|
|
903
|
-
response.variety =
|
|
900
|
+
response.encoding = "binary"
|
|
901
|
+
response.source = Buffer.from(result.body, "base64")
|
|
902
|
+
response.variety = "buffer"
|
|
904
903
|
} else {
|
|
905
|
-
if (result && result.body && typeof result.body !==
|
|
904
|
+
if (result && result.body && typeof result.body !== "string") {
|
|
906
905
|
// FIXME TODO we should probably just write to console instead of returning a payload
|
|
907
906
|
return this.#reply502(
|
|
908
907
|
response,
|
|
909
|
-
|
|
908
|
+
"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",
|
|
910
909
|
{},
|
|
911
910
|
)
|
|
912
911
|
}
|
|
@@ -951,12 +950,12 @@ export default class HttpServer {
|
|
|
951
950
|
let hapiPath
|
|
952
951
|
|
|
953
952
|
if (httpEvent.isHttpApi) {
|
|
954
|
-
if (httpEvent.routeKey ===
|
|
955
|
-
method =
|
|
953
|
+
if (httpEvent.routeKey === "$default") {
|
|
954
|
+
method = "ANY"
|
|
956
955
|
path = httpEvent.routeKey
|
|
957
|
-
hapiPath =
|
|
956
|
+
hapiPath = "/{default*}"
|
|
958
957
|
} else {
|
|
959
|
-
;[method, path] = httpEvent.routeKey.split(
|
|
958
|
+
;[method, path] = httpEvent.routeKey.split(" ")
|
|
960
959
|
hapiPath = generateHapiPath(
|
|
961
960
|
path,
|
|
962
961
|
{
|
|
@@ -980,7 +979,7 @@ export default class HttpServer {
|
|
|
980
979
|
).generate()
|
|
981
980
|
|
|
982
981
|
const stage = endpoint.isHttpApi
|
|
983
|
-
?
|
|
982
|
+
? "$default"
|
|
984
983
|
: this.#options.stage || this.#serverless.service.provider.stage
|
|
985
984
|
|
|
986
985
|
const protectedRoute = httpEvent.private
|
|
@@ -988,7 +987,7 @@ export default class HttpServer {
|
|
|
988
987
|
: undefined
|
|
989
988
|
|
|
990
989
|
const { host, httpPort, httpsProtocol } = this.#options
|
|
991
|
-
const server = `${httpsProtocol ?
|
|
990
|
+
const server = `${httpsProtocol ? "https" : "http"}://${host}:${httpPort}`
|
|
992
991
|
|
|
993
992
|
this.#terminalInfo.push({
|
|
994
993
|
invokePath: `/2015-03-31/functions/${functionKey}/invocations`,
|
|
@@ -1032,15 +1031,15 @@ export default class HttpServer {
|
|
|
1032
1031
|
}
|
|
1033
1032
|
}
|
|
1034
1033
|
|
|
1035
|
-
const hapiMethod = method ===
|
|
1034
|
+
const hapiMethod = method === "ANY" ? "*" : method
|
|
1036
1035
|
|
|
1037
1036
|
const state = this.#options.disableCookieValidation
|
|
1038
1037
|
? {
|
|
1039
|
-
failAction:
|
|
1038
|
+
failAction: "ignore",
|
|
1040
1039
|
parse: false,
|
|
1041
1040
|
}
|
|
1042
1041
|
: {
|
|
1043
|
-
failAction:
|
|
1042
|
+
failAction: "error",
|
|
1044
1043
|
parse: true,
|
|
1045
1044
|
}
|
|
1046
1045
|
|
|
@@ -1058,15 +1057,15 @@ export default class HttpServer {
|
|
|
1058
1057
|
|
|
1059
1058
|
// skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
|
|
1060
1059
|
// for more details, check https://github.com/dherault/serverless-offline/issues/204
|
|
1061
|
-
if (hapiMethod ===
|
|
1060
|
+
if (hapiMethod === "HEAD") {
|
|
1062
1061
|
log.notice(
|
|
1063
|
-
|
|
1062
|
+
"HEAD method event detected. Skipping HAPI server route mapping",
|
|
1064
1063
|
)
|
|
1065
1064
|
|
|
1066
1065
|
return
|
|
1067
1066
|
}
|
|
1068
1067
|
|
|
1069
|
-
if (hapiMethod !==
|
|
1068
|
+
if (hapiMethod !== "HEAD" && hapiMethod !== "GET") {
|
|
1070
1069
|
// maxBytes: Increase request size from 1MB default limit to 10MB.
|
|
1071
1070
|
// Cf AWS API GW payload limits.
|
|
1072
1071
|
hapiOptions.payload = {
|
|
@@ -1080,7 +1079,7 @@ export default class HttpServer {
|
|
|
1080
1079
|
additionalRequestContext.operationName = httpEvent.operationId
|
|
1081
1080
|
}
|
|
1082
1081
|
|
|
1083
|
-
hapiOptions.tags = [
|
|
1082
|
+
hapiOptions.tags = ["api"]
|
|
1084
1083
|
|
|
1085
1084
|
const hapiHandler = this.#createHapiHandler({
|
|
1086
1085
|
additionalRequestContext,
|
|
@@ -1106,14 +1105,14 @@ export default class HttpServer {
|
|
|
1106
1105
|
|
|
1107
1106
|
log.error(error)
|
|
1108
1107
|
|
|
1109
|
-
response.header(
|
|
1108
|
+
response.header("Content-Type", "application/json")
|
|
1110
1109
|
|
|
1111
1110
|
response.statusCode = statusCode
|
|
1112
1111
|
response.source = {
|
|
1113
1112
|
errorMessage: message,
|
|
1114
1113
|
errorType: error.constructor.name,
|
|
1115
1114
|
offlineInfo:
|
|
1116
|
-
|
|
1115
|
+
"If you believe this is an issue with serverless-offline please submit it, thanks. https://github.com/dherault/serverless-offline/issues",
|
|
1117
1116
|
stackTrace: this.#getArrayStackTrace(error.stack),
|
|
1118
1117
|
}
|
|
1119
1118
|
|
|
@@ -1146,7 +1145,7 @@ export default class HttpServer {
|
|
|
1146
1145
|
log.notice()
|
|
1147
1146
|
|
|
1148
1147
|
log.notice()
|
|
1149
|
-
log.notice(
|
|
1148
|
+
log.notice("Routes defined in resources:")
|
|
1150
1149
|
|
|
1151
1150
|
entries(resourceRoutes).forEach(([methodId, resourceRoutesObj]) => {
|
|
1152
1151
|
const { isProxy, method, pathResource, proxyUri } = resourceRoutesObj
|
|
@@ -1178,15 +1177,15 @@ export default class HttpServer {
|
|
|
1178
1177
|
return
|
|
1179
1178
|
}
|
|
1180
1179
|
|
|
1181
|
-
const hapiMethod = method ===
|
|
1180
|
+
const hapiMethod = method === "ANY" ? "*" : method
|
|
1182
1181
|
|
|
1183
1182
|
const state = this.#options.disableCookieValidation
|
|
1184
1183
|
? {
|
|
1185
|
-
failAction:
|
|
1184
|
+
failAction: "ignore",
|
|
1186
1185
|
parse: false,
|
|
1187
1186
|
}
|
|
1188
1187
|
: {
|
|
1189
|
-
failAction:
|
|
1188
|
+
failAction: "error",
|
|
1190
1189
|
parse: true,
|
|
1191
1190
|
}
|
|
1192
1191
|
|
|
@@ -1197,15 +1196,15 @@ export default class HttpServer {
|
|
|
1197
1196
|
|
|
1198
1197
|
// skip HEAD routes as hapi will fail with 'Method name not allowed: HEAD ...'
|
|
1199
1198
|
// for more details, check https://github.com/dherault/serverless-offline/issues/204
|
|
1200
|
-
if (hapiMethod ===
|
|
1199
|
+
if (hapiMethod === "HEAD") {
|
|
1201
1200
|
log.notice(
|
|
1202
|
-
|
|
1201
|
+
"HEAD method event detected. Skipping HAPI server route mapping",
|
|
1203
1202
|
)
|
|
1204
1203
|
|
|
1205
1204
|
return
|
|
1206
1205
|
}
|
|
1207
1206
|
|
|
1208
|
-
if (hapiMethod !==
|
|
1207
|
+
if (hapiMethod !== "GET" && hapiMethod !== "HEAD") {
|
|
1209
1208
|
hapiOptions.payload = {
|
|
1210
1209
|
parse: false,
|
|
1211
1210
|
}
|
|
@@ -1248,14 +1247,14 @@ export default class HttpServer {
|
|
|
1248
1247
|
|
|
1249
1248
|
create404Route() {
|
|
1250
1249
|
// If a {proxy+} or $default route exists, don't conflict with it
|
|
1251
|
-
if (this.#server.match(
|
|
1250
|
+
if (this.#server.match("*", "/{p*}")) {
|
|
1252
1251
|
return
|
|
1253
1252
|
}
|
|
1254
1253
|
|
|
1255
1254
|
const existingRoutes = this.#server
|
|
1256
1255
|
.table()
|
|
1257
1256
|
// Exclude this (404) route
|
|
1258
|
-
.filter((route) => route.path !==
|
|
1257
|
+
.filter((route) => route.path !== "/{p*}")
|
|
1259
1258
|
// Sort by path
|
|
1260
1259
|
.sort((a, b) => (a.path <= b.path ? -1 : 1))
|
|
1261
1260
|
// Human-friendly result
|
|
@@ -1265,7 +1264,7 @@ export default class HttpServer {
|
|
|
1265
1264
|
handler(request, h) {
|
|
1266
1265
|
const response = h.response({
|
|
1267
1266
|
currentRoute: `${request.method} - ${request.path}`,
|
|
1268
|
-
error:
|
|
1267
|
+
error: "Serverless-offline: route not found.",
|
|
1269
1268
|
existingRoutes,
|
|
1270
1269
|
statusCode: 404,
|
|
1271
1270
|
})
|
|
@@ -1273,11 +1272,11 @@ export default class HttpServer {
|
|
|
1273
1272
|
|
|
1274
1273
|
return response
|
|
1275
1274
|
},
|
|
1276
|
-
method:
|
|
1275
|
+
method: "*",
|
|
1277
1276
|
options: {
|
|
1278
1277
|
cors: this.#options.corsConfig,
|
|
1279
1278
|
},
|
|
1280
|
-
path:
|
|
1279
|
+
path: "/{p*}",
|
|
1281
1280
|
}
|
|
1282
1281
|
|
|
1283
1282
|
this.#server.route(route)
|
|
@@ -1286,7 +1285,7 @@ export default class HttpServer {
|
|
|
1286
1285
|
#getArrayStackTrace(stack) {
|
|
1287
1286
|
if (!stack) return null
|
|
1288
1287
|
|
|
1289
|
-
const splittedStack = stack.split(
|
|
1288
|
+
const splittedStack = stack.split("\n")
|
|
1290
1289
|
|
|
1291
1290
|
return splittedStack
|
|
1292
1291
|
.slice(
|