lambder 6.0.2 → 7.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/CHANGELOG.md +2316 -0
- package/README.md +60 -33
- package/dist/api/LambderApiAnswer.d.ts +40 -0
- package/dist/api/LambderApiAnswer.js +19 -0
- package/dist/api/LambderApiCallContext.d.ts +38 -0
- package/dist/api/LambderApiCallContext.js +13 -0
- package/dist/api/LambderApiDefinition.d.ts +18 -0
- package/dist/api/LambderApiDefinition.js +1 -0
- package/dist/api/LambderApiEnvelope.d.ts +67 -0
- package/dist/api/LambderApiEnvelope.js +180 -0
- package/dist/api/LambderApiGuards.d.ts +302 -0
- package/dist/api/LambderApiGuards.js +134 -0
- package/dist/api/LambderApiIdempotency.d.ts +122 -0
- package/dist/api/LambderApiIdempotency.js +330 -0
- package/dist/api/LambderApiPipeline.d.ts +134 -0
- package/dist/api/LambderApiPipeline.js +221 -0
- package/dist/api/LambderApiPolicyEngine.d.ts +36 -0
- package/dist/api/LambderApiPolicyEngine.js +77 -0
- package/dist/api/LambderApiRateLimits.d.ts +206 -0
- package/dist/api/LambderApiRateLimits.js +239 -0
- package/dist/api/LambderApiRequest.d.ts +101 -0
- package/dist/api/LambderApiRequest.js +129 -0
- package/dist/api/LambderApiValidationRefusal.d.ts +32 -0
- package/dist/api/LambderApiValidationRefusal.js +40 -0
- package/dist/client/LambderCaller.d.ts +62 -55
- package/dist/client/LambderCaller.js +147 -90
- package/dist/client/lambderFetchTransport.d.ts +9 -0
- package/dist/client/lambderFetchTransport.js +71 -0
- package/dist/client.d.ts +20 -10
- package/dist/client.js +11 -5
- package/dist/core/Lambder.d.ts +117 -253
- package/dist/core/Lambder.js +374 -341
- package/dist/core/LambderContext.d.ts +54 -44
- package/dist/core/LambderContext.js +41 -110
- package/dist/core/LambderCreateOptions.d.ts +285 -0
- package/dist/core/LambderCreateOptions.js +44 -0
- package/dist/core/LambderFiles.d.ts +1 -45
- package/dist/core/LambderFiles.js +18 -38
- package/dist/core/LambderIndexHtml.d.ts +37 -0
- package/dist/core/LambderIndexHtml.js +87 -0
- package/dist/core/LambderPolicyBuilders.d.ts +17 -0
- package/dist/core/LambderPolicyBuilders.js +16 -0
- package/dist/core/LambderPublicFiles.d.ts +5 -2
- package/dist/core/LambderPublicFiles.js +7 -2
- package/dist/core/LambderResolver.d.ts +8 -6
- package/dist/core/LambderResponse.d.ts +29 -11
- package/dist/core/LambderResponse.js +96 -49
- package/dist/core/LambderResponseBuilder.d.ts +18 -14
- package/dist/core/LambderResponseBuilder.js +19 -25
- package/dist/core/LambderRouting.d.ts +18 -7
- package/dist/core/LambderRouting.js +17 -7
- package/dist/core/LambderTemplatingEngine.d.ts +0 -62
- package/dist/core/LambderTemplatingEngine.js +7 -3
- package/dist/index.d.ts +85 -32
- package/dist/index.js +44 -16
- package/dist/invoke/LambderInvokeCaller.d.ts +46 -139
- package/dist/invoke/LambderInvokeCaller.js +140 -335
- package/dist/invoke/LambderInvokeOutcome.d.ts +165 -0
- package/dist/invoke/LambderInvokeOutcome.js +129 -0
- package/dist/invoke/LambderLambdaEvent.d.ts +81 -0
- package/dist/invoke/LambderLambdaEvent.js +187 -0
- package/dist/invoke/lambderHandlerTransport.d.ts +36 -0
- package/dist/invoke/lambderHandlerTransport.js +89 -0
- package/dist/mock/LambderMockApp.d.ts +352 -0
- package/dist/mock/LambderMockApp.js +815 -0
- package/dist/mock/LambderMockBrowserCookies.d.ts +55 -0
- package/dist/mock/LambderMockBrowserCookies.js +76 -0
- package/dist/mock/LambderMockCallRecorder.d.ts +85 -0
- package/dist/mock/LambderMockCallRecorder.js +183 -0
- package/dist/mock/LambderMockCreateOptions.d.ts +161 -0
- package/dist/mock/LambderMockCreateOptions.js +9 -0
- package/dist/mock/LambderMockEntryRegistry.d.ts +52 -0
- package/dist/mock/LambderMockEntryRegistry.js +126 -0
- package/dist/mock/LambderMockFailureInjector.d.ts +60 -0
- package/dist/mock/LambderMockFailureInjector.js +138 -0
- package/dist/mock/LambderMockTypes.d.ts +421 -0
- package/dist/mock/LambderMockTypes.js +8 -0
- package/dist/mock/lambderMockConsoleLogger.d.ts +16 -0
- package/dist/mock/lambderMockConsoleLogger.js +35 -0
- package/dist/mock/lambderMockInvokeTransport.d.ts +50 -0
- package/dist/mock/lambderMockInvokeTransport.js +52 -0
- package/dist/mock/lambderMockMswHandler.d.ts +99 -0
- package/dist/mock/lambderMockMswHandler.js +126 -0
- package/dist/mock.d.ts +34 -0
- package/dist/mock.js +27 -0
- package/dist/session/LambderSessionController.d.ts +199 -30
- package/dist/session/LambderSessionController.js +396 -82
- package/dist/session/LambderSessionCrypto.d.ts +66 -0
- package/dist/session/LambderSessionCrypto.js +101 -0
- package/dist/session/LambderSessionManager.d.ts +118 -80
- package/dist/session/LambderSessionManager.js +212 -184
- package/dist/shared/LambderI18n.d.ts +6 -6
- package/dist/shared/LambderI18n.js +1 -1
- package/dist/shared/contracts/LambderFileSource.d.ts +33 -0
- package/dist/shared/contracts/LambderFileSource.js +19 -0
- package/dist/shared/contracts/LambderIdempotencyStore.d.ts +66 -0
- package/dist/shared/contracts/LambderIdempotencyStore.js +12 -0
- package/dist/shared/contracts/LambderRateLimiter.d.ts +71 -0
- package/dist/shared/contracts/LambderRateLimiter.js +24 -0
- package/dist/shared/contracts/LambderSessionStore.d.ts +72 -0
- package/dist/shared/contracts/LambderSessionStore.js +13 -0
- package/dist/shared/transport/LambderApiTransport.d.ts +139 -0
- package/dist/shared/transport/LambderApiTransport.js +65 -0
- package/dist/shared/transport/LambderCookieJar.d.ts +121 -0
- package/dist/shared/transport/LambderCookieJar.js +246 -0
- package/dist/shared/transport/lambderCookieJarTransport.d.ts +30 -0
- package/dist/shared/transport/lambderCookieJarTransport.js +60 -0
- package/dist/shared/util/LambderBase64.d.ts +10 -0
- package/dist/shared/util/LambderBase64.js +27 -0
- package/dist/shared/util/LambderCallAbort.d.ts +62 -0
- package/dist/shared/util/LambderCallAbort.js +80 -0
- package/dist/shared/util/LambderClientIp.d.ts +32 -0
- package/dist/shared/util/LambderClientIp.js +56 -0
- package/dist/shared/util/LambderExpiringMap.d.ts +119 -0
- package/dist/shared/util/LambderExpiringMap.js +217 -0
- package/dist/shared/util/LambderKeyFields.d.ts +32 -0
- package/dist/shared/util/LambderKeyFields.js +34 -0
- package/dist/shared/util/LambderNodeModules.d.ts +9 -0
- package/dist/shared/util/LambderNodeModules.js +39 -0
- package/dist/shared/util/LambderOptionChecks.d.ts +17 -0
- package/dist/shared/util/LambderOptionChecks.js +33 -0
- package/dist/shared/util/LambderResponseBrand.d.ts +20 -0
- package/dist/shared/util/LambderResponseBrand.js +18 -0
- package/dist/shared/util/LambderTextDigest.d.ts +17 -0
- package/dist/shared/util/LambderTextDigest.js +34 -0
- package/dist/shared/util/LambderTypeUtilities.d.ts +33 -0
- package/dist/shared/util/LambderTypeUtilities.js +8 -0
- package/dist/shared/wire/LambderAnswerHeaders.d.ts +60 -0
- package/dist/shared/wire/LambderAnswerHeaders.js +94 -0
- package/dist/shared/wire/LambderApiContract.d.ts +129 -0
- package/dist/shared/wire/LambderApiOptionValues.d.ts +39 -0
- package/dist/shared/wire/LambderApiOptionValues.js +11 -0
- package/dist/shared/wire/LambderApiOutcome.d.ts +128 -0
- package/dist/shared/{LambderApiOutcome.js → wire/LambderApiOutcome.js} +16 -9
- package/dist/shared/{LambderApiError.d.ts → wire/LambderApiRefusal.d.ts} +48 -26
- package/dist/shared/{LambderApiError.js → wire/LambderApiRefusal.js} +13 -11
- package/dist/shared/wire/LambderCallOptions.d.ts +171 -0
- package/dist/shared/wire/LambderCallOptions.js +17 -0
- package/dist/shared/{LambderCompressionCodec.d.ts → wire/LambderCompressionCodec.d.ts} +10 -6
- package/dist/shared/{LambderCompressionCodec.js → wire/LambderCompressionCodec.js} +67 -23
- package/dist/shared/{LambderCompressionOption.d.ts → wire/LambderCompressionOption.d.ts} +1 -1
- package/dist/shared/{LambderCompressionOption.js → wire/LambderCompressionOption.js} +3 -4
- package/dist/shared/{LambderCrashDetail.d.ts → wire/LambderCrashDetail.d.ts} +10 -0
- package/dist/shared/{LambderCrashDetail.js → wire/LambderCrashDetail.js} +30 -0
- package/dist/shared/wire/LambderHttpStatus.d.ts +12 -0
- package/dist/shared/wire/LambderHttpStatus.js +1 -0
- package/dist/shared/{LambderRequestPayload.d.ts → wire/LambderRequestPayload.d.ts} +25 -17
- package/dist/shared/{LambderRequestPayload.js → wire/LambderRequestPayload.js} +29 -52
- package/dist/shared/wire/LambderSessionCookieNames.d.ts +9 -0
- package/dist/shared/wire/LambderSessionCookieNames.js +9 -0
- package/dist/stores/LambderDdbCache.d.ts +12 -9
- package/dist/stores/LambderDdbCache.js +56 -47
- package/dist/stores/{LambderDdbIdempotency.d.ts → LambderDdbIdempotencyStore.d.ts} +41 -31
- package/dist/stores/LambderDdbIdempotencyStore.js +319 -0
- package/dist/stores/LambderDdbRateLimiter.d.ts +30 -49
- package/dist/stores/LambderDdbRateLimiter.js +47 -45
- package/dist/stores/LambderDdbSdk.d.ts +83 -6
- package/dist/stores/LambderDdbSdk.js +83 -2
- package/dist/stores/LambderDdbSessionStore.d.ts +65 -0
- package/dist/stores/LambderDdbSessionStore.js +161 -0
- package/dist/stores/LambderHttpFileSource.d.ts +1 -1
- package/dist/stores/LambderHttpFileSource.js +10 -1
- package/dist/stores/LambderLocalFileSource.d.ts +15 -0
- package/dist/stores/LambderLocalFileSource.js +28 -0
- package/dist/stores/LambderMemoryIdempotencyStore.d.ts +63 -0
- package/dist/stores/LambderMemoryIdempotencyStore.js +113 -0
- package/dist/stores/LambderMemoryRateLimiter.d.ts +34 -0
- package/dist/stores/LambderMemoryRateLimiter.js +64 -0
- package/dist/stores/LambderMemorySessionStore.d.ts +48 -0
- package/dist/stores/LambderMemorySessionStore.js +74 -0
- package/dist/stores/LambderS3FileSource.d.ts +1 -1
- package/dist/stores/LambderS3FileSource.js +1 -1
- package/package.json +21 -19
- package/dist/client/LambderMSW.d.ts +0 -69
- package/dist/client/LambderMSW.js +0 -121
- package/dist/policies/LambderApiGuards.d.ts +0 -256
- package/dist/policies/LambderApiGuards.js +0 -94
- package/dist/policies/LambderApiIdempotency.d.ts +0 -58
- package/dist/policies/LambderApiIdempotency.js +0 -219
- package/dist/policies/LambderApiPolicies.d.ts +0 -42
- package/dist/policies/LambderApiPolicies.js +0 -52
- package/dist/policies/LambderApiRateLimits.d.ts +0 -132
- package/dist/policies/LambderApiRateLimits.js +0 -119
- package/dist/shared/LambderApiContract.d.ts +0 -57
- package/dist/shared/LambderApiOutcome.d.ts +0 -69
- package/dist/shared/LambderCallOptions.d.ts +0 -71
- package/dist/shared/LambderCallOptions.js +0 -16
- package/dist/shared/node-polyfills.d.ts +0 -4
- package/dist/shared/node-polyfills.js +0 -58
- package/dist/stores/LambderDdbIdempotency.js +0 -229
- package/dist/testing.d.ts +0 -9
- package/dist/testing.js +0 -8
- /package/dist/shared/{LambderApiContract.js → wire/LambderApiContract.js} +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.d.ts +0 -0
- /package/dist/{core → shared/wire}/LambderCookie.js +0 -0
|
@@ -20,234 +20,22 @@
|
|
|
20
20
|
* LambderInvokeCaller.localTransport runs a callee's handler in-process for
|
|
21
21
|
* tests.
|
|
22
22
|
*/
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
/** The value of the marker header; a future incompatible event shape would bump it. */
|
|
34
|
-
export const LAMBDER_INVOKE_PROTOCOL = "1";
|
|
23
|
+
import { classifyDeliveryFailure, describeFailure, errorFromFunctionError, LambderInvokeError, parseFunctionError, } from "./LambderInvokeOutcome.js";
|
|
24
|
+
import { DEFAULT_SESSION_TOKEN_COOKIE_KEY } from "../shared/wire/LambderSessionCookieNames.js";
|
|
25
|
+
import { resolveApiOutcome } from "../shared/wire/LambderApiOutcome.js";
|
|
26
|
+
import { mergeGuardInputs, } from "../shared/wire/LambderCallOptions.js";
|
|
27
|
+
import { createCallAbort, stopWaitingWhenAborted } from "../shared/util/LambderCallAbort.js";
|
|
28
|
+
import { coerceToError, errorFromCrashDetail } from "../shared/wire/LambderCrashDetail.js";
|
|
29
|
+
import { assertPositiveInteger } from "../shared/util/LambderOptionChecks.js";
|
|
30
|
+
import { resolveCompressionOption } from "../shared/wire/LambderCompressionOption.js";
|
|
31
|
+
import { DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS, DEFAULT_MAX_RESTORED_PAYLOAD_BYTES, compressPayloadBrotli, resolveRequestCompressionMinBytes, } from "../shared/wire/LambderRequestPayload.js";
|
|
32
|
+
import { buildEnvelopeJson, decodeLambdaHttpResult, localLambdaContext, sessionCookies, synthesizeLambdaHttpEvent, } from "./LambderLambdaEvent.js";
|
|
35
33
|
/**
|
|
36
34
|
* Lambda caps a synchronous invoke's request and its response at about 6MB;
|
|
37
35
|
* the same guard threshold finalizeResponse applies to an answer, applied
|
|
38
36
|
* here to the event before it is sent.
|
|
39
37
|
*/
|
|
40
38
|
export const LAMBDER_INVOKE_MAX_EVENT_BYTES = 5_500_000;
|
|
41
|
-
/** Request Brotli when `requestCompression: true`: the HTTP request threshold, at the quality every other Lambder site uses. */
|
|
42
|
-
export const DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS = { minBytes: 4096, quality: 5 };
|
|
43
|
-
const DEFAULT_SESSION_TOKEN_COOKIE_KEY = "LMDRSESSIONTKID";
|
|
44
|
-
/**
|
|
45
|
-
* What api() throws. Its message names the function, the API and the reason,
|
|
46
|
-
* so an error reporter that fingerprints on the message groups one broken
|
|
47
|
-
* API into one row; its cause is the callee's own error rebuilt from the
|
|
48
|
-
* crash detail (or Lambda's FunctionError, or the SDK's rejection), so a
|
|
49
|
-
* reporter that walks causes stores the callee's stack.
|
|
50
|
-
*/
|
|
51
|
-
export class LambderInvokeError extends Error {
|
|
52
|
-
/** Brand for detection across duplicate lambder installs, like LambderApiError. */
|
|
53
|
-
isLambderInvokeError = true;
|
|
54
|
-
reason;
|
|
55
|
-
apiName;
|
|
56
|
-
functionName;
|
|
57
|
-
status;
|
|
58
|
-
errorMessage;
|
|
59
|
-
crash;
|
|
60
|
-
functionError;
|
|
61
|
-
logList;
|
|
62
|
-
zodError;
|
|
63
|
-
retryAfterSeconds;
|
|
64
|
-
bytes;
|
|
65
|
-
/** The full failure outcome; it carries this error and this error carries it. */
|
|
66
|
-
outcome;
|
|
67
|
-
constructor(init) {
|
|
68
|
-
super(init.message, init.cause !== undefined ? { cause: init.cause } : undefined);
|
|
69
|
-
this.name = "LambderInvokeError";
|
|
70
|
-
this.reason = init.reason;
|
|
71
|
-
this.apiName = init.apiName;
|
|
72
|
-
this.functionName = init.functionName;
|
|
73
|
-
this.status = init.status;
|
|
74
|
-
this.errorMessage = init.errorMessage;
|
|
75
|
-
this.crash = init.crash;
|
|
76
|
-
this.functionError = init.functionError;
|
|
77
|
-
this.logList = init.logList;
|
|
78
|
-
this.zodError = init.zodError;
|
|
79
|
-
this.retryAfterSeconds = init.retryAfterSeconds;
|
|
80
|
-
this.bytes = init.bytes;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
/** Brand-based type guard (see LambderInvokeError.isLambderInvokeError). */
|
|
84
|
-
export const isLambderInvokeError = (err) => err instanceof Error && err.isLambderInvokeError === true;
|
|
85
|
-
const randomRequestId = () => {
|
|
86
|
-
const webCrypto = globalThis.crypto;
|
|
87
|
-
if (webCrypto?.randomUUID)
|
|
88
|
-
return webCrypto.randomUUID();
|
|
89
|
-
return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`;
|
|
90
|
-
};
|
|
91
|
-
/** The payload-format-2.0 event API Gateway would deliver for this request. */
|
|
92
|
-
const synthesizeHttpEvent = (request) => {
|
|
93
|
-
const headers = {
|
|
94
|
-
host: request.host,
|
|
95
|
-
"accept-encoding": "br, gzip",
|
|
96
|
-
[LAMBDER_INVOKE_HEADER]: LAMBDER_INVOKE_PROTOCOL,
|
|
97
|
-
};
|
|
98
|
-
const invokedBy = typeof process !== "undefined" ? process.env?.AWS_LAMBDA_FUNCTION_NAME : undefined;
|
|
99
|
-
if (invokedBy)
|
|
100
|
-
headers[LAMBDER_INVOKED_BY_HEADER] = invokedBy;
|
|
101
|
-
if (request.clientIp)
|
|
102
|
-
headers["x-forwarded-for"] = request.clientIp;
|
|
103
|
-
for (const [key, value] of Object.entries(request.headers ?? {}))
|
|
104
|
-
headers[key.toLowerCase()] = value;
|
|
105
|
-
const isBinary = Buffer.isBuffer(request.body);
|
|
106
|
-
if (request.body !== undefined && !headers["content-type"]) {
|
|
107
|
-
headers["content-type"] = isBinary ? "application/octet-stream" : "application/json";
|
|
108
|
-
}
|
|
109
|
-
const now = Date.now();
|
|
110
|
-
return {
|
|
111
|
-
version: "2.0",
|
|
112
|
-
routeKey: "$default",
|
|
113
|
-
rawPath: request.path,
|
|
114
|
-
rawQueryString: new URLSearchParams(request.query ?? {}).toString(),
|
|
115
|
-
headers,
|
|
116
|
-
...(request.cookies?.length ? { cookies: request.cookies } : {}),
|
|
117
|
-
requestContext: {
|
|
118
|
-
accountId: "",
|
|
119
|
-
apiId: "lambder-invoke",
|
|
120
|
-
domainName: request.host,
|
|
121
|
-
domainPrefix: "",
|
|
122
|
-
http: {
|
|
123
|
-
method: request.method,
|
|
124
|
-
path: request.path,
|
|
125
|
-
protocol: "HTTP/1.1",
|
|
126
|
-
sourceIp: request.clientIp ?? "",
|
|
127
|
-
userAgent: "lambder-invoke",
|
|
128
|
-
},
|
|
129
|
-
requestId: randomRequestId(),
|
|
130
|
-
routeKey: "$default",
|
|
131
|
-
stage: "$default",
|
|
132
|
-
time: new Date(now).toISOString(),
|
|
133
|
-
timeEpoch: now,
|
|
134
|
-
},
|
|
135
|
-
...(request.body !== undefined
|
|
136
|
-
? { body: isBinary ? request.body.toString("base64") : request.body }
|
|
137
|
-
: {}),
|
|
138
|
-
isBase64Encoded: isBinary,
|
|
139
|
-
};
|
|
140
|
-
};
|
|
141
|
-
/**
|
|
142
|
-
* The body envelope LambderCaller sends, minus the fields only a browser has
|
|
143
|
-
* a value for, as JSON. A plain payload arrives already serialized (the
|
|
144
|
-
* compression decision needed its JSON) and is spliced in rather than
|
|
145
|
-
* parsed and stringified a second time; a compressed one rides as its two
|
|
146
|
-
* fields.
|
|
147
|
-
*/
|
|
148
|
-
const buildEnvelopeJson = (fields) => {
|
|
149
|
-
const withoutPayload = JSON.stringify({
|
|
150
|
-
apiName: fields.apiName,
|
|
151
|
-
version: fields.version,
|
|
152
|
-
token: fields.csrf ?? "",
|
|
153
|
-
siteHost: fields.siteHost,
|
|
154
|
-
...(fields.compressed ?? {}),
|
|
155
|
-
...(fields.guardInputs !== undefined ? { guardInputs: fields.guardInputs } : {}),
|
|
156
|
-
...(fields.idempotencyKey !== undefined ? { idempotencyKey: fields.idempotencyKey } : {}),
|
|
157
|
-
});
|
|
158
|
-
if (fields.payloadJson === undefined)
|
|
159
|
-
return withoutPayload;
|
|
160
|
-
return `${withoutPayload.slice(0, -1)},"payload":${fields.payloadJson}}`;
|
|
161
|
-
};
|
|
162
|
-
const sessionCookies = (session, tokenCookieKey) => session ? [`${tokenCookieKey}=${session.token}`] : undefined;
|
|
163
|
-
/**
|
|
164
|
-
* Brotli one payload's JSON for sending, or null when the plain JSON should
|
|
165
|
-
* go instead: the browser's compressPayloadGzip with Brotli, because both
|
|
166
|
-
* ends are Node. The threshold and the only-when-smaller rule are
|
|
167
|
-
* compressPayloadWith's, shared with the gzip side.
|
|
168
|
-
*/
|
|
169
|
-
export const compressPayloadBrotli = (json, minBytes, quality) => compressPayloadWith(json, minBytes, COMPRESSED_PAYLOAD_BR_FIELD, (bytes) => compressText(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength), "br", quality));
|
|
170
|
-
/** A timeout controller chained to an external signal, so either source aborts the invoke. */
|
|
171
|
-
const abortAfter = (timeoutMs, external) => {
|
|
172
|
-
let timedOut = false;
|
|
173
|
-
let signal = external;
|
|
174
|
-
let timeoutId;
|
|
175
|
-
// The forwarding listener is detached in clear(), not left to `once`: an
|
|
176
|
-
// external signal usually outlives the call (a request-scoped one passed
|
|
177
|
-
// to several invokes, an app-lifetime one), so a listener per call would
|
|
178
|
-
// accumulate on it for as long as it lives.
|
|
179
|
-
let detach;
|
|
180
|
-
if (timeoutMs !== undefined) {
|
|
181
|
-
const controller = new AbortController();
|
|
182
|
-
if (external) {
|
|
183
|
-
if (external.aborted) {
|
|
184
|
-
controller.abort(external.reason);
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
const forward = () => controller.abort(external.reason);
|
|
188
|
-
external.addEventListener("abort", forward, { once: true });
|
|
189
|
-
detach = () => external.removeEventListener("abort", forward);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
timeoutId = setTimeout(() => { timedOut = true; controller.abort(); }, timeoutMs);
|
|
193
|
-
signal = controller.signal;
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
signal,
|
|
197
|
-
timedOut: () => timedOut,
|
|
198
|
-
clear: () => { if (timeoutId !== undefined)
|
|
199
|
-
clearTimeout(timeoutId); detach?.(); },
|
|
200
|
-
};
|
|
201
|
-
};
|
|
202
|
-
const errorFromFunctionError = (functionError) => {
|
|
203
|
-
const error = new Error(functionError.errorMessage ?? "the function failed");
|
|
204
|
-
error.name = functionError.errorType ?? "FunctionError";
|
|
205
|
-
if (functionError.trace?.length)
|
|
206
|
-
error.stack = functionError.trace.join("\n");
|
|
207
|
-
return error;
|
|
208
|
-
};
|
|
209
|
-
const parseFunctionError = (result) => {
|
|
210
|
-
if (result && typeof result === "object") {
|
|
211
|
-
const { errorType, errorMessage, trace } = result;
|
|
212
|
-
return {
|
|
213
|
-
...(typeof errorType === "string" ? { errorType } : {}),
|
|
214
|
-
...(typeof errorMessage === "string" ? { errorMessage } : {}),
|
|
215
|
-
...(Array.isArray(trace) ? { trace: trace.map(String) } : {}),
|
|
216
|
-
};
|
|
217
|
-
}
|
|
218
|
-
return { errorMessage: typeof result === "string" ? result : undefined };
|
|
219
|
-
};
|
|
220
|
-
/** The one-line detail a failure's message ends with. */
|
|
221
|
-
const describeFailure = (init) => {
|
|
222
|
-
if (init.crash)
|
|
223
|
-
return init.crash.message;
|
|
224
|
-
if (init.functionError)
|
|
225
|
-
return `${init.functionError.errorType ?? "FunctionError"}: ${init.functionError.errorMessage ?? "the function failed"}`;
|
|
226
|
-
if (init.errorMessage !== undefined) {
|
|
227
|
-
const content = init.errorMessage?.content;
|
|
228
|
-
if (typeof content === "string")
|
|
229
|
-
return content;
|
|
230
|
-
if (typeof init.errorMessage === "string")
|
|
231
|
-
return init.errorMessage;
|
|
232
|
-
try {
|
|
233
|
-
return JSON.stringify(init.errorMessage);
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
return String(init.errorMessage);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
if (init.reason === 'validation')
|
|
240
|
-
return "the callee rejected the input";
|
|
241
|
-
if (init.reason === 'versionExpired')
|
|
242
|
-
return "the callee answered versionExpired";
|
|
243
|
-
if (init.reason === 'sessionExpired')
|
|
244
|
-
return "the callee answered sessionExpired";
|
|
245
|
-
if (init.reason === 'notAuthorized')
|
|
246
|
-
return "the callee answered notAuthorized";
|
|
247
|
-
if (init.cause instanceof Error)
|
|
248
|
-
return init.cause.message;
|
|
249
|
-
return init.status !== undefined ? `HTTP ${init.status}` : "no answer";
|
|
250
|
-
};
|
|
251
39
|
/**
|
|
252
40
|
* @typeParam TContract - The callee's API contract (`typeof lambder.ApiContract`, imported type-only), for typed names, payloads, results and guard inputs.
|
|
253
41
|
* @typeParam TProvidedGuards - Guard names guardInputsProvider covers; those APIs' options argument becomes optional.
|
|
@@ -280,10 +68,7 @@ export default class LambderInvokeCaller {
|
|
|
280
68
|
this.host = host ?? functionName;
|
|
281
69
|
// `?? false`: like the browser caller, off unless asked for.
|
|
282
70
|
this.requestCompression = resolveCompressionOption(requestCompression ?? false, DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS);
|
|
283
|
-
this.maxResponsePayloadBytes = maxResponsePayloadBytes ?? DEFAULT_MAX_RESTORED_PAYLOAD_BYTES;
|
|
284
|
-
if (!Number.isSafeInteger(this.maxResponsePayloadBytes) || this.maxResponsePayloadBytes <= 0) {
|
|
285
|
-
throw new Error("LambderInvokeCaller: maxResponsePayloadBytes must be a positive integer");
|
|
286
|
-
}
|
|
71
|
+
this.maxResponsePayloadBytes = assertPositiveInteger(maxResponsePayloadBytes ?? DEFAULT_MAX_RESTORED_PAYLOAD_BYTES, "LambderInvokeCaller maxResponsePayloadBytes");
|
|
287
72
|
this.timeoutMs = timeoutMs;
|
|
288
73
|
this.onLogList = onLogList;
|
|
289
74
|
this.onFailure = onFailure;
|
|
@@ -298,7 +83,7 @@ export default class LambderInvokeCaller {
|
|
|
298
83
|
static createEvent(init) {
|
|
299
84
|
const host = init.host ?? "lambder-invoke";
|
|
300
85
|
const tokenCookieKey = init.sessionTokenCookieKey ?? DEFAULT_SESSION_TOKEN_COOKIE_KEY;
|
|
301
|
-
return
|
|
86
|
+
return synthesizeLambdaHttpEvent({
|
|
302
87
|
method: "POST",
|
|
303
88
|
path: init.apiPath ?? "/api",
|
|
304
89
|
host,
|
|
@@ -314,35 +99,34 @@ export default class LambderInvokeCaller {
|
|
|
314
99
|
guardInputs: init.guardInputs,
|
|
315
100
|
idempotencyKey: init.idempotencyKey,
|
|
316
101
|
}),
|
|
317
|
-
});
|
|
102
|
+
}, { invoke: true });
|
|
318
103
|
}
|
|
319
104
|
/**
|
|
320
105
|
* A transport that runs a callee's handler in this process, the way
|
|
321
106
|
* Lambda would: a thrown error becomes a FunctionError payload. For
|
|
322
107
|
* tests that want the real handlers behind the real envelope.
|
|
108
|
+
*
|
|
109
|
+
* It honours the signal the way lambderHandlerTransport does, by ending
|
|
110
|
+
* the wait: a function call in this process cannot be cancelled, so the
|
|
111
|
+
* handler runs to completion regardless and what a timeout buys is the
|
|
112
|
+
* caller's answer. Ignoring it made timeoutMs a no-op here.
|
|
323
113
|
*/
|
|
324
114
|
static localTransport(handler, context = {}) {
|
|
325
|
-
return async (event, { functionName }) => {
|
|
326
|
-
|
|
327
|
-
callbackWaitsForEmptyEventLoop: false,
|
|
328
|
-
functionName,
|
|
329
|
-
functionVersion: "$LATEST",
|
|
330
|
-
invokedFunctionArn: `arn:aws:lambda:local:000000000000:function:${functionName}`,
|
|
331
|
-
memoryLimitInMB: "128",
|
|
332
|
-
awsRequestId: randomRequestId(),
|
|
333
|
-
logGroupName: `/aws/lambda/${functionName}`,
|
|
334
|
-
logStreamName: "local",
|
|
335
|
-
getRemainingTimeInMillis: () => 30_000,
|
|
336
|
-
done: () => { },
|
|
337
|
-
fail: () => { },
|
|
338
|
-
succeed: () => { },
|
|
339
|
-
...context,
|
|
340
|
-
};
|
|
115
|
+
return async (event, { functionName, signal }) => {
|
|
116
|
+
signal?.throwIfAborted();
|
|
341
117
|
try {
|
|
342
|
-
return {
|
|
118
|
+
return {
|
|
119
|
+
functionError: null,
|
|
120
|
+
result: await stopWaitingWhenAborted(handler(event, localLambdaContext(functionName, context)), signal),
|
|
121
|
+
};
|
|
343
122
|
}
|
|
344
123
|
catch (err) {
|
|
345
|
-
|
|
124
|
+
// An abort is the caller giving up, not the callee failing:
|
|
125
|
+
// reporting it as a FunctionError would turn a timeout into a
|
|
126
|
+
// crash outcome with an invented error payload.
|
|
127
|
+
if (signal?.aborted && err === signal.reason)
|
|
128
|
+
throw err;
|
|
129
|
+
const error = coerceToError(err, "the handler failed");
|
|
346
130
|
return {
|
|
347
131
|
functionError: "Unhandled",
|
|
348
132
|
result: { errorType: error.name, errorMessage: error.message, trace: (error.stack ?? "").split("\n") },
|
|
@@ -360,8 +144,11 @@ export default class LambderInvokeCaller {
|
|
|
360
144
|
}
|
|
361
145
|
async invokeThroughSdk(eventJson, signal) {
|
|
362
146
|
const { LambdaClient, InvokeCommand } = await this.loadSdk();
|
|
147
|
+
// One call is one delivery attempt, which is what LambderApiTransport
|
|
148
|
+
// promises: the SDK's own default of 3 would re-invoke a callee that
|
|
149
|
+
// already ran when only the response was lost.
|
|
363
150
|
if (!this.client)
|
|
364
|
-
this.client = new LambdaClient(this.clientConfig
|
|
151
|
+
this.client = new LambdaClient({ maxAttempts: 1, ...this.clientConfig });
|
|
365
152
|
const output = await this.client.send(new InvokeCommand({
|
|
366
153
|
FunctionName: this.functionName,
|
|
367
154
|
InvocationType: "RequestResponse",
|
|
@@ -379,55 +166,51 @@ export default class LambderInvokeCaller {
|
|
|
379
166
|
}
|
|
380
167
|
return { functionError: output.FunctionError ?? null, result };
|
|
381
168
|
}
|
|
382
|
-
/** Delivers one event, serialized exactly once; a rejected transport
|
|
383
|
-
async
|
|
384
|
-
|
|
169
|
+
/** Delivers one event, serialized exactly once; an event over the invoke cap, a rejected transport, or one that answered after the call was given up on, is a failure. */
|
|
170
|
+
async deliverEvent(event, eventJson, options) {
|
|
171
|
+
// Measured here rather than on the API path alone, because every
|
|
172
|
+
// caller of this one sends the same bytes. A path that skips the cap
|
|
173
|
+
// gets the SDK's RequestEntityTooLargeException back instead, which
|
|
174
|
+
// classifies as `protocol` and names neither the size nor the cap.
|
|
175
|
+
const bytes = Buffer.byteLength(eventJson, "utf8");
|
|
176
|
+
if (bytes > LAMBDER_INVOKE_MAX_EVENT_BYTES) {
|
|
177
|
+
return { failed: {
|
|
178
|
+
reason: 'payloadTooLarge', bytes,
|
|
179
|
+
detail: `the event is ${bytes} bytes, over the ${LAMBDER_INVOKE_MAX_EVENT_BYTES} byte invoke cap`,
|
|
180
|
+
} };
|
|
181
|
+
}
|
|
182
|
+
// The same wiring the browser caller uses, so the two cannot drift on
|
|
183
|
+
// what a late or abandoned call means.
|
|
184
|
+
const abort = createCallAbort({ timeoutMs: options.timeoutMs ?? this.timeoutMs, signal: options.signal });
|
|
385
185
|
try {
|
|
186
|
+
// A call the site has already given up on does not reach the
|
|
187
|
+
// transport: honouring the signal is the transport's obligation
|
|
188
|
+
// and not every transport does.
|
|
189
|
+
const refused = abort.abortFailure("beforeSending");
|
|
190
|
+
if (refused)
|
|
191
|
+
return { failed: { reason: refused.reason, cause: refused.error, detail: refused.error.message } };
|
|
386
192
|
const sent = await this.transport(event, { functionName: this.functionName, eventJson, signal: abort.signal });
|
|
193
|
+
// An answer that arrives after the abort is not a success: a
|
|
194
|
+
// transport that ignores the signal resolves late, and believing
|
|
195
|
+
// it would report ok on a 20ms timeoutMs at 300ms, handing the
|
|
196
|
+
// call site data it had already abandoned.
|
|
197
|
+
const late = abort.abortFailure("afterAnswering");
|
|
198
|
+
if (late)
|
|
199
|
+
return { failed: { reason: late.reason, cause: late.error, detail: late.error.message } };
|
|
387
200
|
return { sent };
|
|
388
201
|
}
|
|
389
202
|
catch (err) {
|
|
390
|
-
const cause = err
|
|
391
|
-
|
|
203
|
+
const cause = coerceToError(err, "the invoke failed");
|
|
204
|
+
// The caller's own timeout wins, since only it knows about that;
|
|
205
|
+
// otherwise the rejection says what it was.
|
|
206
|
+
return { failed: { reason: abort.timedOut() ? 'timeout' : classifyDeliveryFailure(cause), cause } };
|
|
392
207
|
}
|
|
393
208
|
finally {
|
|
394
|
-
abort.
|
|
209
|
+
abort.detach();
|
|
395
210
|
}
|
|
396
211
|
}
|
|
397
|
-
/** The function's answer as an HTTP result; throws when it is not one, or its compressed body cannot be restored. */
|
|
398
|
-
async decodeHttpResult(result) {
|
|
399
|
-
if (!result || typeof result !== "object" || typeof result.statusCode !== "number") {
|
|
400
|
-
throw new Error("the function did not answer with an HTTP response object; is it a Lambder app?");
|
|
401
|
-
}
|
|
402
|
-
const raw = result;
|
|
403
|
-
const headers = {};
|
|
404
|
-
for (const [key, value] of Object.entries(raw.headers ?? {}))
|
|
405
|
-
headers[key.toLowerCase()] = value;
|
|
406
|
-
for (const [key, values] of Object.entries(raw.multiValueHeaders ?? {}))
|
|
407
|
-
headers[key.toLowerCase()] = values.join(", ");
|
|
408
|
-
let body = raw.body ? Buffer.from(raw.body, raw.isBase64Encoded ? "base64" : "utf8") : Buffer.alloc(0);
|
|
409
|
-
const encoding = headers["content-encoding"]?.trim().toLowerCase();
|
|
410
|
-
if (encoding === "br" || encoding === "gzip") {
|
|
411
|
-
// restoreBytes, not restoreText: a route may answer compressed
|
|
412
|
-
// binary (a wasm module, anything it forced compression on), and
|
|
413
|
-
// decoding that as UTF-8 first would replace every byte that is
|
|
414
|
-
// not valid UTF-8 and hand back a silently different body.
|
|
415
|
-
body = await restoreBytes(body, encoding, { maxBytes: this.maxResponsePayloadBytes });
|
|
416
|
-
}
|
|
417
|
-
else if (encoding) {
|
|
418
|
-
throw new Error(`the answer carries an unsupported Content-Encoding "${encoding}"`);
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
statusCode: raw.statusCode,
|
|
422
|
-
headers,
|
|
423
|
-
cookies: raw.cookies ?? [],
|
|
424
|
-
body,
|
|
425
|
-
text: () => body.toString("utf8"),
|
|
426
|
-
json: () => JSON.parse(body.toString("utf8")),
|
|
427
|
-
};
|
|
428
|
-
}
|
|
429
212
|
/** Builds the failure and its error, reports it once, and hands it back. */
|
|
430
|
-
async
|
|
213
|
+
async failureOutcome(apiName, init) {
|
|
431
214
|
const logList = init.logList ?? [];
|
|
432
215
|
const cause = init.crash ? errorFromCrashDetail(init.crash)
|
|
433
216
|
: init.functionError ? errorFromFunctionError(init.functionError)
|
|
@@ -448,11 +231,16 @@ export default class LambderInvokeCaller {
|
|
|
448
231
|
bytes: init.bytes,
|
|
449
232
|
cause,
|
|
450
233
|
});
|
|
234
|
+
// FailureInit's arms mirror the outcome's, so each reason's evidence
|
|
235
|
+
// was already demanded at the site that chose the reason; the
|
|
236
|
+
// assembly is one object either way, and this is where it is named as
|
|
237
|
+
// the arm it is rather than written out five times.
|
|
451
238
|
const failure = {
|
|
452
239
|
ok: false,
|
|
453
240
|
reason: init.reason,
|
|
454
241
|
error,
|
|
455
242
|
logList,
|
|
243
|
+
cookies: init.cookies ?? [],
|
|
456
244
|
...(init.status !== undefined ? { status: init.status } : {}),
|
|
457
245
|
...(init.errorMessage !== undefined ? { errorMessage: init.errorMessage } : {}),
|
|
458
246
|
...(init.retryAfterSeconds !== undefined ? { retryAfterSeconds: init.retryAfterSeconds } : {}),
|
|
@@ -460,9 +248,8 @@ export default class LambderInvokeCaller {
|
|
|
460
248
|
...(init.crash !== undefined ? { crash: init.crash } : {}),
|
|
461
249
|
...(init.functionError !== undefined ? { functionError: init.functionError } : {}),
|
|
462
250
|
...(init.bytes !== undefined ? { bytes: init.bytes } : {}),
|
|
251
|
+
...(init.response !== undefined ? { response: init.response } : {}),
|
|
463
252
|
};
|
|
464
|
-
if (init.response !== undefined)
|
|
465
|
-
failure.response = init.response;
|
|
466
253
|
error.outcome = failure;
|
|
467
254
|
if (this.onFailure) {
|
|
468
255
|
// A reporting hook that breaks must not turn apiOutcome() into a
|
|
@@ -502,7 +289,6 @@ export default class LambderInvokeCaller {
|
|
|
502
289
|
// reported. So a throw here is an 'unknown' failure like any other.
|
|
503
290
|
let event;
|
|
504
291
|
let eventJson;
|
|
505
|
-
let bytes;
|
|
506
292
|
try {
|
|
507
293
|
// Provider values underneath, per-call values on top.
|
|
508
294
|
const provided = this.guardInputsProvider
|
|
@@ -514,13 +300,11 @@ export default class LambderInvokeCaller {
|
|
|
514
300
|
// into the envelope. Compressed when enabled and the JSON reaches
|
|
515
301
|
// the threshold; `compressRequest` overrides both ways.
|
|
516
302
|
const payloadJson = payload !== undefined ? JSON.stringify(payload) : undefined;
|
|
517
|
-
const compressionMinBytes = options.compressRequest
|
|
518
|
-
: options.compressRequest === false ? null
|
|
519
|
-
: this.requestCompression?.minBytes ?? null;
|
|
303
|
+
const compressionMinBytes = resolveRequestCompressionMinBytes(options.compressRequest, this.requestCompression);
|
|
520
304
|
const compressed = compressionMinBytes !== null && payloadJson !== undefined
|
|
521
305
|
? await compressPayloadBrotli(payloadJson, compressionMinBytes, this.requestCompression?.quality ?? DEFAULT_INVOKE_REQUEST_COMPRESSION_SETTINGS.quality)
|
|
522
306
|
: null;
|
|
523
|
-
event =
|
|
307
|
+
event = synthesizeLambdaHttpEvent({
|
|
524
308
|
method: "POST",
|
|
525
309
|
path: this.apiPath,
|
|
526
310
|
host: this.host,
|
|
@@ -537,33 +321,26 @@ export default class LambderInvokeCaller {
|
|
|
537
321
|
guardInputs,
|
|
538
322
|
idempotencyKey: options.idempotencyKey,
|
|
539
323
|
}),
|
|
540
|
-
});
|
|
324
|
+
}, { invoke: true });
|
|
541
325
|
// Serialized once here; the size guard and the SDK transport both use it.
|
|
542
326
|
eventJson = JSON.stringify(event);
|
|
543
|
-
bytes = Buffer.byteLength(eventJson, "utf8");
|
|
544
327
|
}
|
|
545
328
|
catch (err) {
|
|
546
|
-
return await this.
|
|
329
|
+
return await this.failureOutcome(apiName, { reason: 'unknown', cause: coerceToError(err, "the call could not be built") });
|
|
547
330
|
}
|
|
548
|
-
|
|
549
|
-
return await this.fail(apiName, {
|
|
550
|
-
reason: 'payloadTooLarge', bytes,
|
|
551
|
-
detail: `the event is ${bytes} bytes, over the ${LAMBDER_INVOKE_MAX_EVENT_BYTES} byte invoke cap`,
|
|
552
|
-
});
|
|
553
|
-
}
|
|
554
|
-
const delivery = await this.deliver(event, eventJson, options);
|
|
331
|
+
const delivery = await this.deliverEvent(event, eventJson, options);
|
|
555
332
|
if ("failed" in delivery)
|
|
556
|
-
return await this.
|
|
333
|
+
return await this.failureOutcome(apiName, delivery.failed);
|
|
557
334
|
if (delivery.sent.functionError) {
|
|
558
|
-
return await this.
|
|
335
|
+
return await this.failureOutcome(apiName, { reason: 'crash', functionError: parseFunctionError(delivery.sent.result) });
|
|
559
336
|
}
|
|
560
337
|
let http;
|
|
561
338
|
try {
|
|
562
|
-
http = await
|
|
339
|
+
http = await decodeLambdaHttpResult(delivery.sent.result, this.maxResponsePayloadBytes);
|
|
563
340
|
}
|
|
564
341
|
catch (err) {
|
|
565
|
-
const cause = err
|
|
566
|
-
return await this.
|
|
342
|
+
const cause = coerceToError(err, "the answer could not be decoded");
|
|
343
|
+
return await this.failureOutcome(apiName, { reason: 'protocol', cause, detail: cause.message });
|
|
567
344
|
}
|
|
568
345
|
const outcome = await resolveApiOutcome({
|
|
569
346
|
status: http.statusCode,
|
|
@@ -571,37 +348,64 @@ export default class LambderInvokeCaller {
|
|
|
571
348
|
json: async () => http.json(),
|
|
572
349
|
text: async () => http.text(),
|
|
573
350
|
});
|
|
574
|
-
|
|
351
|
+
// Every answer's logs, from the one field the mapping puts them on:
|
|
352
|
+
// an envelope's, a 500 body's, and a rejected input's, which the
|
|
353
|
+
// callee writes onto the validation body as it does onto a success.
|
|
354
|
+
const logList = outcome.logList ?? [];
|
|
575
355
|
await this.surfaceLogs(apiName, logList);
|
|
356
|
+
// The answer's Set-Cookie values, so a session the callee rotated or
|
|
357
|
+
// cleared is visible to whoever is carrying it.
|
|
358
|
+
const cookies = http.cookies;
|
|
576
359
|
// The declared output, by the callee's own typing: res.api(null) compiles
|
|
577
360
|
// only for an output that allows null or beside a reason (an errorMessage
|
|
578
361
|
// is a failure below; a message-only null is the callee's contract to keep).
|
|
579
362
|
if (outcome.ok)
|
|
580
|
-
return { ok: true, payload: (outcome.payload ?? null), response: outcome.response, logList };
|
|
581
|
-
|
|
582
|
-
//
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
363
|
+
return { ok: true, payload: (outcome.payload ?? null), response: outcome.response, logList, cookies };
|
|
364
|
+
const shared = { status: outcome.status, retryAfterSeconds: outcome.retryAfterSeconds, logList, cookies };
|
|
365
|
+
// Each failure reason carries different evidence, and the outcome
|
|
366
|
+
// union says which: a rejected input has its issues and no envelope,
|
|
367
|
+
// an envelope refusal has the envelope and no Error.
|
|
368
|
+
if (outcome.reason === 'validation') {
|
|
369
|
+
return await this.failureOutcome(apiName, { ...shared, reason: 'validation', zodError: outcome.zodError });
|
|
370
|
+
}
|
|
371
|
+
if (outcome.reason === 'server') {
|
|
372
|
+
// A 404 text page is what a callee answers when apiPath does not
|
|
373
|
+
// match: the one misconfiguration every first integration hits.
|
|
374
|
+
const detail = http.statusCode === 404
|
|
375
|
+
? `no API at ${this.apiPath} on ${this.functionName} (HTTP 404): does apiPath match the callee's?`
|
|
376
|
+
: undefined;
|
|
377
|
+
return await this.failureOutcome(apiName, {
|
|
378
|
+
...shared,
|
|
379
|
+
reason: 'server',
|
|
380
|
+
errorMessage: outcome.errorMessage,
|
|
381
|
+
response: outcome.response,
|
|
382
|
+
crash: outcome.response?.crash,
|
|
383
|
+
cause: outcome.error,
|
|
384
|
+
detail,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return await this.failureOutcome(apiName, {
|
|
388
|
+
...shared,
|
|
587
389
|
reason: outcome.reason,
|
|
588
|
-
status: outcome.status,
|
|
589
390
|
errorMessage: outcome.errorMessage,
|
|
590
|
-
retryAfterSeconds: outcome.retryAfterSeconds,
|
|
591
|
-
zodError: outcome.zodError,
|
|
592
391
|
response: outcome.response,
|
|
593
|
-
crash: outcome.response
|
|
594
|
-
logList,
|
|
595
|
-
cause: outcome.error,
|
|
596
|
-
detail,
|
|
392
|
+
crash: outcome.response.crash,
|
|
597
393
|
});
|
|
598
394
|
}
|
|
599
395
|
/**
|
|
600
396
|
* Full-fidelity call: resolves to a discriminated LambderInvokeOutcome
|
|
601
397
|
* instead of throwing. Never throws; for sites that degrade gracefully.
|
|
398
|
+
*
|
|
399
|
+
* The output is computed from the contract in the return type rather than
|
|
400
|
+
* taken as a type parameter, so a call site cannot replace it by
|
|
401
|
+
* annotating what it assigns to.
|
|
602
402
|
*/
|
|
603
|
-
async apiOutcome(apiName,
|
|
604
|
-
|
|
403
|
+
async apiOutcome(apiName, ...rest) {
|
|
404
|
+
// The tuple is a conditional type on an unresolved TApiName, so its
|
|
405
|
+
// elements read as unknown from inside; the contract shaped them on
|
|
406
|
+
// the way in, which is where the guarantee belongs.
|
|
407
|
+
const [payload, options] = rest;
|
|
408
|
+
return await this.dispatch(apiName, payload, options);
|
|
605
409
|
}
|
|
606
410
|
/**
|
|
607
411
|
* The declared output, or a thrown LambderInvokeError carrying the
|
|
@@ -612,8 +416,9 @@ export default class LambderInvokeCaller {
|
|
|
612
416
|
* reason (LambderApiAnswer), so a nullable output is the one place null
|
|
613
417
|
* arrives.
|
|
614
418
|
*/
|
|
615
|
-
async api(apiName,
|
|
616
|
-
const
|
|
419
|
+
async api(apiName, ...rest) {
|
|
420
|
+
const [payload, options] = rest;
|
|
421
|
+
const outcome = await this.dispatch(apiName, payload, options);
|
|
617
422
|
if (!outcome.ok)
|
|
618
423
|
throw outcome.error;
|
|
619
424
|
return outcome.payload;
|
|
@@ -627,7 +432,7 @@ export default class LambderInvokeCaller {
|
|
|
627
432
|
async request(init) {
|
|
628
433
|
const method = (init.method ?? "GET").toUpperCase();
|
|
629
434
|
const name = `${method} ${init.path}`;
|
|
630
|
-
const event =
|
|
435
|
+
const event = synthesizeLambdaHttpEvent({
|
|
631
436
|
method,
|
|
632
437
|
path: init.path,
|
|
633
438
|
query: init.query,
|
|
@@ -636,19 +441,19 @@ export default class LambderInvokeCaller {
|
|
|
636
441
|
clientIp: init.clientIp,
|
|
637
442
|
cookies: init.cookies,
|
|
638
443
|
body: init.body,
|
|
639
|
-
});
|
|
640
|
-
const delivery = await this.
|
|
444
|
+
}, { invoke: true });
|
|
445
|
+
const delivery = await this.deliverEvent(event, JSON.stringify(event), init);
|
|
641
446
|
if ("failed" in delivery)
|
|
642
|
-
throw (await this.
|
|
447
|
+
throw (await this.failureOutcome(name, delivery.failed)).error;
|
|
643
448
|
if (delivery.sent.functionError) {
|
|
644
|
-
throw (await this.
|
|
449
|
+
throw (await this.failureOutcome(name, { reason: 'crash', functionError: parseFunctionError(delivery.sent.result) })).error;
|
|
645
450
|
}
|
|
646
451
|
try {
|
|
647
|
-
return await
|
|
452
|
+
return await decodeLambdaHttpResult(delivery.sent.result, this.maxResponsePayloadBytes);
|
|
648
453
|
}
|
|
649
454
|
catch (err) {
|
|
650
|
-
const cause = err
|
|
651
|
-
throw (await this.
|
|
455
|
+
const cause = coerceToError(err, "the answer could not be decoded");
|
|
456
|
+
throw (await this.failureOutcome(name, { reason: 'protocol', cause, detail: cause.message })).error;
|
|
652
457
|
}
|
|
653
458
|
}
|
|
654
459
|
}
|