@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.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/bin/cliOperations.ts +12 -0
- package/bin/restart.ts +66 -6
- package/components/Application.ts +1134 -112
- package/components/OptionsWatcher.ts +368 -102
- package/components/Scope.ts +20 -1
- package/components/componentLoader.ts +43 -4
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/operations.js +284 -52
- package/components/operationsValidation.js +49 -2
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/config/RootConfigWatcher.ts +191 -37
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +78 -26
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +4 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
- package/dist/bin/cliOperations.js +13 -0
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/restart.js +42 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/components/Application.d.ts +104 -9
- package/dist/components/Application.js +954 -102
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -1
- package/dist/components/OptionsWatcher.js +378 -104
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.js +15 -1
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +35 -3
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +263 -54
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +48 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +2 -0
- package/dist/config/RootConfigWatcher.js +189 -35
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +10 -9
- package/dist/config/configUtils.js +63 -27
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +3 -0
- package/dist/resources/DatabaseTransaction.d.ts +25 -0
- package/dist/resources/DatabaseTransaction.js +224 -18
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +22 -3
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +22 -6
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +1 -1
- package/dist/resources/RecordEncoder.js +21 -5
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +97 -13
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -0
- package/dist/resources/RocksIndexStore.js +2 -1
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
- package/dist/resources/RocksTransactionLogStore.js +104 -33
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +58 -7
- package/dist/resources/Table.js +1095 -346
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +170 -0
- package/dist/resources/auditStore.js +457 -11
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +16 -13
- package/dist/resources/databases.js +624 -176
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.js +3 -2
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +848 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +47 -4
- package/dist/resources/recordLock.js +138 -7
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2565 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.js +5 -0
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +160 -40
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/security/jsLoader.js +6 -4
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +33 -2
- package/dist/server/REST.js.map +1 -1
- package/dist/server/http.d.ts +5 -1
- package/dist/server/http.js +34 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +2 -0
- package/dist/server/serverHelpers/Headers.js +6 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
- package/dist/server/serverHelpers/contentTypes.js +189 -15
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +96 -17
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/storageReclamation.js +1 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +64 -6
- package/dist/server/threads/manageThreads.js +261 -12
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +196 -13
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +30 -7
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +24 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.js +217 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +172 -92
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/npmUtilities.js +6 -4
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/watcherFallback.d.ts +0 -45
- package/dist/utility/watcherFallback.js +1 -125
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.js +6 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +6 -0
- package/json/systemSchema.json +3 -0
- package/npm-shrinkwrap.json +131 -41
- package/package.json +10 -3
- package/resources/DESIGN.md +124 -19
- package/resources/DatabaseTransaction.ts +230 -17
- package/resources/LMDBTransaction.ts +21 -3
- package/resources/PrimaryRocksDatabase.ts +20 -7
- package/resources/RecordEncoder.ts +25 -5
- package/resources/Resource.ts +97 -13
- package/resources/ResourceInterface.ts +8 -0
- package/resources/RocksIndexStore.ts +2 -1
- package/resources/RocksTransactionLogStore.ts +111 -31
- package/resources/Table.ts +1224 -393
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +460 -11
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +610 -146
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +3 -2
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +173 -7
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +5 -0
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +169 -49
- package/resources/tracked.ts +73 -22
- package/security/jsLoader.ts +6 -4
- package/server/DESIGN.md +11 -0
- package/server/REST.ts +36 -3
- package/server/http.ts +34 -2
- package/server/serverHelpers/Headers.ts +5 -1
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +188 -15
- package/server/serverHelpers/serverUtilities.ts +143 -24
- package/server/storageReclamation.ts +2 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +254 -12
- package/server/threads/socketRouter.ts +217 -11
- package/server/threads/threadServer.js +30 -7
- package/studio/web/assets/{Chat-BnCBegQz.js → Chat-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/errors/hdbError.ts +54 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/logging/harper_logger.ts +209 -30
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +196 -91
- package/utility/npmUtilities.ts +6 -4
- package/utility/watcherFallback.ts +0 -122
- package/validation/configValidator.ts +6 -3
package/server/DESIGN.md
CHANGED
|
@@ -208,6 +208,17 @@ mirror because it only widens what an allowlist may _name_; enforcement stays on
|
|
|
208
208
|
|
|
209
209
|
`REST.ts → http(request, nextHandler)` is the chief integration point: it takes a `Request`, asks the `Resources` registry for a match, builds a `RequestTarget`, and dispatches into the Resource class's static method. Cache headers are translated to `request.expiresAt` / `onlyIfCached` / `noCache` flags within the same function.
|
|
210
210
|
|
|
211
|
+
### Streaming startup errors
|
|
212
|
+
|
|
213
|
+
SSE and NDJSON serializers eagerly take and hold their first iterator step for GET requests. `REST.ts` waits through the next event-loop turn for that step: an immediate rejection remains an HTTP error rendered as Problem Details, while a first item or the cutoff commits the stream. Mutating requests do not use the startup-status gate because their transaction has already committed by this point; all of their stream failures use the in-band form. Later GET failures are terminal, format-valid records (`event: harper-error` for SSE and a reserved control record for NDJSON). Keep the decision in the serializer/REST boundary so Node, uWS, Bun, compression, and injection share one contract; transports must not independently prefetch the iterator.
|
|
214
|
+
|
|
215
|
+
SSE terminal event data uses `{ error: <code-or-class>, message: <message>, status?: <status> }`; NDJSON wraps the same object as `{ "$harperStreamError": { ... } }` so it cannot be mistaken for an ordinary row with an `error` field.
|
|
216
|
+
Sources with `mapError` remain unmapped for these two formats so their failures reach this single contract instead of becoming legacy data records.
|
|
217
|
+
generic JSON-array streaming retains its older `{ error: "<name>: <message>" }` element shape.
|
|
218
|
+
The `error` value is the stable programmatic discriminator; `message` is diagnostic and follows the same thrown-message exposure policy as pre-commit Problem Details.
|
|
219
|
+
|
|
220
|
+
Clean stream completion does not prove completeness; clients must inspect SSE for the `harper-error` event and NDJSON for the `$harperStreamError` control record.
|
|
221
|
+
|
|
211
222
|
### Deferred credential rejection (#2418)
|
|
212
223
|
|
|
213
224
|
`authentication` runs before route matching, so when it meets an `Authorization` header it cannot
|
package/server/REST.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
serialize,
|
|
5
|
+
serializeMessage,
|
|
6
|
+
getDeserializer,
|
|
7
|
+
waitForStreamStartup,
|
|
8
|
+
discardSerializedStream,
|
|
9
|
+
} from '../server/serverHelpers/contentTypes.ts';
|
|
4
10
|
import { addAnalyticsListener, recordAction, recordActionBinary } from '../resources/analytics/write.ts';
|
|
5
11
|
import * as harperLogger from '../utility/logging/harper_logger.ts';
|
|
6
12
|
import { ServerError, ClientError } from '../utility/errors/hdbError.ts';
|
|
@@ -348,7 +354,15 @@ async function http(request: Request, nextHandler, resources: Resources, httpOpt
|
|
|
348
354
|
headers.setIfNone('Last-Modified', new Date(lastModification).toUTCString());
|
|
349
355
|
} else if (responseData.headers) {
|
|
350
356
|
// if response is a Response object (or response-like envelope with headers), use it as the response
|
|
351
|
-
|
|
357
|
+
const response = finalizeResponse(responseData, headers, status, request);
|
|
358
|
+
if (request.method === 'HEAD') {
|
|
359
|
+
discardSerializedStream(response.body);
|
|
360
|
+
if (!(response instanceof Response)) response.body = undefined;
|
|
361
|
+
} else if (request.method === 'GET') {
|
|
362
|
+
const startup = waitForStreamStartup(response.body);
|
|
363
|
+
if (startup) await startup;
|
|
364
|
+
}
|
|
365
|
+
return response;
|
|
352
366
|
} else if (isFinite(lastModification)) {
|
|
353
367
|
etagFloat[0] = lastModification;
|
|
354
368
|
// base64 encoding of the 64-bit float encoding of the date in ms (with quotes)
|
|
@@ -422,7 +436,13 @@ async function http(request: Request, nextHandler, resources: Resources, httpOpt
|
|
|
422
436
|
setCountHeaders(headers, (target as any).offset || 0, (target as any).count, responseData);
|
|
423
437
|
}
|
|
424
438
|
responseObject.body = serialize(responseData, request, responseObject);
|
|
425
|
-
if (method === 'HEAD')
|
|
439
|
+
if (request.method === 'HEAD') {
|
|
440
|
+
discardSerializedStream(responseObject.body);
|
|
441
|
+
responseObject.body = undefined; // we want everything else to be the same as GET, but then omit the body
|
|
442
|
+
} else if (request.method === 'GET') {
|
|
443
|
+
const startup = waitForStreamStartup(responseObject.body);
|
|
444
|
+
if (startup) await startup;
|
|
445
|
+
}
|
|
426
446
|
}
|
|
427
447
|
// A collection read's count headers vary by the request's `Prefer` value; serialize() just reset
|
|
428
448
|
// `Vary`, so declare it here (after serialization) — otherwise a shared cache could serve count
|
|
@@ -466,6 +486,19 @@ async function http(request: Request, nextHandler, resources: Resources, httpOpt
|
|
|
466
486
|
headers,
|
|
467
487
|
body: undefined,
|
|
468
488
|
};
|
|
489
|
+
// The error body is a different representation, so success representation and caching headers
|
|
490
|
+
// accumulated before the startup failure must not survive its serialization.
|
|
491
|
+
for (const header of [
|
|
492
|
+
'Content-Encoding',
|
|
493
|
+
'Cache-Control',
|
|
494
|
+
'ETag',
|
|
495
|
+
'Last-Modified',
|
|
496
|
+
'Content-Range',
|
|
497
|
+
'Range-Unit',
|
|
498
|
+
'Preference-Applied',
|
|
499
|
+
'Age',
|
|
500
|
+
])
|
|
501
|
+
headers.delete(header);
|
|
469
502
|
responseObject.body = serialize(problemDetail, request, responseObject);
|
|
470
503
|
return responseObject;
|
|
471
504
|
}
|
package/server/http.ts
CHANGED
|
@@ -12,6 +12,11 @@ import * as env from '../utility/environment/environmentManager.ts';
|
|
|
12
12
|
import * as terms from '../utility/hdbTerms.ts';
|
|
13
13
|
import { getConfigPath } from '../config/configUtils.ts';
|
|
14
14
|
import { getTicketKeys, getWorkerIndex } from './threads/manageThreads.js';
|
|
15
|
+
import {
|
|
16
|
+
applicationSocketName,
|
|
17
|
+
isolatedApplicationRoute,
|
|
18
|
+
thisThreadsIsolatedApplication,
|
|
19
|
+
} from './threads/isolatedApplications.ts';
|
|
15
20
|
import { createTLSSelector, getEffectiveTlsCiphers } from '../security/keys.ts';
|
|
16
21
|
import { createSecureServer, createServer as createH2CServer } from 'node:http2';
|
|
17
22
|
import { createServer as createSecureServerHttp1 } from 'node:https';
|
|
@@ -196,10 +201,21 @@ export function writeUdsMetadata(
|
|
|
196
201
|
port: number | string,
|
|
197
202
|
secureServer: any,
|
|
198
203
|
protocol?: string,
|
|
199
|
-
mtlsForwarding = true
|
|
204
|
+
mtlsForwarding = true,
|
|
205
|
+
route: { application: string; hosts: string[] } | undefined = isolatedApplicationRoute()
|
|
200
206
|
) {
|
|
201
207
|
const contexts = secureServer.secureContexts;
|
|
202
208
|
let yaml = `pid: ${process.pid}\ntid: ${currentThreadId()}\nport: ${port}\n`;
|
|
209
|
+
// Routing identity, separate from certificate coverage: a proxy sends this application's hosts to
|
|
210
|
+
// this socket alone, whatever hostnames the (shared, possibly wildcard) certificates carry.
|
|
211
|
+
if (route) {
|
|
212
|
+
yaml += `application: ${JSON.stringify(route.application)}\n`;
|
|
213
|
+
if (route.hosts.length === 0) yaml += `applicationHosts: []\n`;
|
|
214
|
+
else {
|
|
215
|
+
yaml += `applicationHosts:\n`;
|
|
216
|
+
for (const host of route.hosts) yaml += ` - ${JSON.stringify(host)}\n`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
203
219
|
// Which application protocol this socket speaks (absent = http/1.1, the historical
|
|
204
220
|
// default) — lets a fronting proxy route by negotiated ALPN.
|
|
205
221
|
if (protocol) yaml += `protocol: ${protocol}\n`;
|
|
@@ -267,6 +283,19 @@ export function cleanupSocketsDirectory() {
|
|
|
267
283
|
} catch {}
|
|
268
284
|
}
|
|
269
285
|
|
|
286
|
+
export function cleanupApplicationSockets(application: string) {
|
|
287
|
+
const socketsDir = join(env.getHdbBasePath(), 'sockets');
|
|
288
|
+
const prefix = applicationSocketName(application, '');
|
|
289
|
+
try {
|
|
290
|
+
for (const file of readdirSync(socketsDir)) {
|
|
291
|
+
if (!file.startsWith(prefix) || (!file.endsWith('.sock') && !file.endsWith('.yaml'))) continue;
|
|
292
|
+
try {
|
|
293
|
+
unlinkSync(join(socketsDir, file));
|
|
294
|
+
} catch {}
|
|
295
|
+
}
|
|
296
|
+
} catch {}
|
|
297
|
+
}
|
|
298
|
+
|
|
270
299
|
// Entries in `universalHeaders` that were pushed by `applySecurityHeaders`, so a config
|
|
271
300
|
// hot-reload can remove exactly the entries it owns without clobbering entries pushed by
|
|
272
301
|
// other components.
|
|
@@ -875,7 +904,10 @@ function getHTTPServer(port: number, secure: boolean, options: ServerOptions) {
|
|
|
875
904
|
if (secure && env.get(terms.CONFIG_PARAMS.TLS_UNIXDOMAINSOCKETS)) {
|
|
876
905
|
const socketsDir = join(env.getHdbBasePath(), 'sockets');
|
|
877
906
|
mkdirSync(socketsDir, { recursive: true });
|
|
878
|
-
const
|
|
907
|
+
const isolatedApplication = thisThreadsIsolatedApplication();
|
|
908
|
+
const socketName = isolatedApplication
|
|
909
|
+
? applicationSocketName(isolatedApplication, port)
|
|
910
|
+
: `${getWorkerIndex()}-${port}`;
|
|
879
911
|
const udsPath = join(socketsDir, `${socketName}.sock`);
|
|
880
912
|
const yamlPath = join(socketsDir, `${socketName}.yaml`);
|
|
881
913
|
|
|
@@ -31,6 +31,10 @@ export class Headers extends Map<string, [string, string | string[]]> {
|
|
|
31
31
|
if (typeof name !== 'string') name = '' + name;
|
|
32
32
|
return super.has(name.toLowerCase());
|
|
33
33
|
}
|
|
34
|
+
delete(name) {
|
|
35
|
+
if (typeof name !== 'string') name = '' + name;
|
|
36
|
+
return super.delete(name.toLowerCase());
|
|
37
|
+
}
|
|
34
38
|
setIfNone(name, value) {
|
|
35
39
|
if (typeof name !== 'string') name = '' + name;
|
|
36
40
|
if (typeof value !== 'string') value = '' + value;
|
|
@@ -239,7 +243,7 @@ function nodeResponseHeaders(nodeResponse: any) {
|
|
|
239
243
|
};
|
|
240
244
|
}
|
|
241
245
|
|
|
242
|
-
function applyWriteHeadHeaders(nodeResponse: any, headers: any): void {
|
|
246
|
+
export function applyWriteHeadHeaders(nodeResponse: any, headers: any): void {
|
|
243
247
|
const suppliedHeaders = new Map<string, { name: string; value: any }>();
|
|
244
248
|
const addHeader = (name: string, value: any) => {
|
|
245
249
|
const key = String(name).toLowerCase();
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { STATUS_CODES } from 'node:http';
|
|
2
|
+
import type {
|
|
3
|
+
IncomingMessage as NodeIncomingMessage,
|
|
4
|
+
OutgoingHttpHeader,
|
|
5
|
+
OutgoingHttpHeaders,
|
|
6
|
+
ServerResponse as NodeServerResponse,
|
|
7
|
+
} from 'node:http';
|
|
8
|
+
import type { Socket } from 'node:net';
|
|
9
|
+
import { PassThrough } from 'node:stream';
|
|
10
|
+
import { Headers as ResponseHeaders, applyWriteHeadHeaders } from './Headers.ts';
|
|
11
|
+
|
|
12
|
+
export interface AdaptedResponse {
|
|
13
|
+
status: number;
|
|
14
|
+
headers: ResponseHeaders;
|
|
15
|
+
body: PassThrough;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
class HeadersSentError extends Error {
|
|
19
|
+
code = 'ERR_HTTP_HEADERS_SENT';
|
|
20
|
+
constructor(action: string) {
|
|
21
|
+
super(`Cannot ${action} headers after they are sent to the client`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class UnsupportedResponseMethodError extends Error {
|
|
26
|
+
constructor(method: string) {
|
|
27
|
+
super(`${method}() is not supported on a withNodeAdapter() response`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const ignoreError = () => {};
|
|
32
|
+
|
|
33
|
+
export class NodeAdapterResponse extends PassThrough implements NodeServerResponse {
|
|
34
|
+
statusCode = 200;
|
|
35
|
+
statusMessage = '';
|
|
36
|
+
strictContentLength = false;
|
|
37
|
+
chunkedEncoding = false;
|
|
38
|
+
shouldKeepAlive = true;
|
|
39
|
+
useChunkedEncodingByDefault = true;
|
|
40
|
+
sendDate = true;
|
|
41
|
+
readonly req: NodeIncomingMessage;
|
|
42
|
+
readonly socket: Socket | null;
|
|
43
|
+
#headers = new ResponseHeaders();
|
|
44
|
+
#committedStatus: number | undefined;
|
|
45
|
+
#headerText: string | undefined;
|
|
46
|
+
#nodeResponse: NodeServerResponse | undefined;
|
|
47
|
+
#resolve: (response: AdaptedResponse) => void;
|
|
48
|
+
#reject: (reason: unknown) => void;
|
|
49
|
+
|
|
50
|
+
constructor(
|
|
51
|
+
req: NodeIncomingMessage,
|
|
52
|
+
nodeResponse: NodeServerResponse | undefined,
|
|
53
|
+
resolve: (response: AdaptedResponse) => void,
|
|
54
|
+
reject: (reason: unknown) => void
|
|
55
|
+
) {
|
|
56
|
+
super();
|
|
57
|
+
this.req = req;
|
|
58
|
+
this.socket = req.socket ?? null;
|
|
59
|
+
this.#nodeResponse = nodeResponse;
|
|
60
|
+
this.#resolve = resolve;
|
|
61
|
+
this.#reject = reject;
|
|
62
|
+
this.on('error', ignoreError);
|
|
63
|
+
if (typeof nodeResponse?.on === 'function') {
|
|
64
|
+
const forward = (...args: any[]) => this.emit('timeout', ...args);
|
|
65
|
+
let forwarding = false;
|
|
66
|
+
const syncForwarding = (wanted: boolean) => {
|
|
67
|
+
if (wanted === forwarding) return;
|
|
68
|
+
forwarding = wanted;
|
|
69
|
+
if (wanted) nodeResponse.on('timeout', forward);
|
|
70
|
+
else nodeResponse.removeListener('timeout', forward);
|
|
71
|
+
};
|
|
72
|
+
const onNewListener = (event: string | symbol) => event === 'timeout' && syncForwarding(true);
|
|
73
|
+
const onRemoveListener = (event: string | symbol) =>
|
|
74
|
+
event === 'timeout' && syncForwarding(this.listenerCount('timeout') > 0);
|
|
75
|
+
this.on('newListener', onNewListener);
|
|
76
|
+
this.on('removeListener', onRemoveListener);
|
|
77
|
+
this.once('close', () => {
|
|
78
|
+
syncForwarding(false);
|
|
79
|
+
this.removeListener('newListener', onNewListener);
|
|
80
|
+
this.removeListener('removeListener', onRemoveListener);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get headersSent() {
|
|
86
|
+
return this.#committedStatus !== undefined;
|
|
87
|
+
}
|
|
88
|
+
get finished() {
|
|
89
|
+
return this.writableEnded;
|
|
90
|
+
}
|
|
91
|
+
get connection() {
|
|
92
|
+
return this.socket;
|
|
93
|
+
}
|
|
94
|
+
get _header(): string | null {
|
|
95
|
+
if (this.#committedStatus === undefined) return null;
|
|
96
|
+
if (this.#headerText === undefined) {
|
|
97
|
+
let text = `HTTP/1.1 ${this.#committedStatus} ${this.statusMessage}\r\n`;
|
|
98
|
+
for (const [name, value] of this.#headers) {
|
|
99
|
+
if (Array.isArray(value)) for (const entry of value) text += `${name}: ${entry}\r\n`;
|
|
100
|
+
else text += `${name}: ${value}\r\n`;
|
|
101
|
+
}
|
|
102
|
+
this.#headerText = text + '\r\n';
|
|
103
|
+
}
|
|
104
|
+
return this.#headerText;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setHeader(name: string, value: number | string | readonly string[]) {
|
|
108
|
+
if (this.headersSent) throw new HeadersSentError('set');
|
|
109
|
+
this.#headers.set(name, value);
|
|
110
|
+
return this;
|
|
111
|
+
}
|
|
112
|
+
setHeaders(headers: Headers | Map<string, number | string | readonly string[]> | ResponseHeaders) {
|
|
113
|
+
let cookies: string[] | undefined;
|
|
114
|
+
for (const [name, value] of headers) {
|
|
115
|
+
if (name.toLowerCase() !== 'set-cookie') this.setHeader(name, value);
|
|
116
|
+
else if (Array.isArray(value)) (cookies ??= []).push(...value);
|
|
117
|
+
else (cookies ??= []).push(String(value));
|
|
118
|
+
}
|
|
119
|
+
if (cookies) this.setHeader('set-cookie', cookies);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
appendHeader(name: string, value: string | readonly string[]) {
|
|
123
|
+
if (this.headersSent) throw new HeadersSentError('append');
|
|
124
|
+
if (Array.isArray(value)) for (const entry of value) this.#headers.append(name, entry);
|
|
125
|
+
else this.#headers.append(name, value);
|
|
126
|
+
return this;
|
|
127
|
+
}
|
|
128
|
+
getHeader(name: string) {
|
|
129
|
+
return this.#headers.get(name);
|
|
130
|
+
}
|
|
131
|
+
getHeaders() {
|
|
132
|
+
const headers: OutgoingHttpHeaders = Object.create(null);
|
|
133
|
+
for (const [name, value] of this.#headers) headers[name.toLowerCase()] = value;
|
|
134
|
+
return headers;
|
|
135
|
+
}
|
|
136
|
+
getHeaderNames() {
|
|
137
|
+
return [...this.#headers.keys()];
|
|
138
|
+
}
|
|
139
|
+
hasHeader(name: string) {
|
|
140
|
+
return this.#headers.has(name);
|
|
141
|
+
}
|
|
142
|
+
removeHeader(name: string) {
|
|
143
|
+
if (this.headersSent) throw new HeadersSentError('remove');
|
|
144
|
+
this.#headers.delete(name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
writeHead(statusCode: number, statusMessage?: string, headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]): this;
|
|
148
|
+
writeHead(statusCode: number, headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]): this;
|
|
149
|
+
writeHead(
|
|
150
|
+
statusCode: number,
|
|
151
|
+
statusMessageOrHeaders?: string | OutgoingHttpHeaders | OutgoingHttpHeader[],
|
|
152
|
+
headers?: OutgoingHttpHeaders | OutgoingHttpHeader[]
|
|
153
|
+
) {
|
|
154
|
+
if (this.headersSent) throw new HeadersSentError('write');
|
|
155
|
+
this.statusCode = statusCode;
|
|
156
|
+
if (typeof statusMessageOrHeaders === 'string') this.statusMessage = statusMessageOrHeaders;
|
|
157
|
+
else {
|
|
158
|
+
this.statusMessage ||= STATUS_CODES[statusCode] || 'unknown';
|
|
159
|
+
// writeHead(302, undefined, headers) is the three-argument form with the status message
|
|
160
|
+
// omitted; the hole must not erase the headers the third argument carries.
|
|
161
|
+
headers ??= statusMessageOrHeaders;
|
|
162
|
+
}
|
|
163
|
+
if (headers) applyWriteHeadHeaders(this, headers);
|
|
164
|
+
this.#committedStatus = statusCode;
|
|
165
|
+
this.#resolve({ status: statusCode, headers: this.#headers, body: this });
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
_implicitHeader() {
|
|
169
|
+
this.writeHead(this.statusCode);
|
|
170
|
+
}
|
|
171
|
+
flushHeaders() {
|
|
172
|
+
if (!this.headersSent) this._implicitHeader();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
write(
|
|
176
|
+
chunk: unknown,
|
|
177
|
+
encoding?: BufferEncoding | ((error?: Error | null) => void),
|
|
178
|
+
callback?: (error?: Error | null) => void
|
|
179
|
+
) {
|
|
180
|
+
if (!this.headersSent) this._implicitHeader();
|
|
181
|
+
return super.write(chunk, encoding as BufferEncoding, callback);
|
|
182
|
+
}
|
|
183
|
+
end(chunk?: unknown, encoding?: BufferEncoding | (() => void), callback?: () => void) {
|
|
184
|
+
if (!this.headersSent) this._implicitHeader();
|
|
185
|
+
return super.end(chunk, encoding as BufferEncoding, callback);
|
|
186
|
+
}
|
|
187
|
+
_destroy(error: Error | null, callback: (error?: Error | null) => void) {
|
|
188
|
+
if (!this.headersSent) this.#reject(error ?? new Error('Response destroyed before headers were sent'));
|
|
189
|
+
callback(error);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
setTimeout(msecs: number, callback?: () => void) {
|
|
193
|
+
if (callback) this.on('timeout', callback);
|
|
194
|
+
const nodeResponse = this.#nodeResponse;
|
|
195
|
+
if (typeof nodeResponse?.setTimeout !== 'function') return this;
|
|
196
|
+
nodeResponse.setTimeout(msecs);
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
writeContinue(callback?: () => void) {
|
|
200
|
+
if (typeof this.#nodeResponse?.writeContinue === 'function') this.#nodeResponse.writeContinue(callback);
|
|
201
|
+
else callback?.();
|
|
202
|
+
}
|
|
203
|
+
writeProcessing(callback?: () => void) {
|
|
204
|
+
if (typeof this.#nodeResponse?.writeProcessing === 'function') this.#nodeResponse.writeProcessing(callback);
|
|
205
|
+
else callback?.();
|
|
206
|
+
}
|
|
207
|
+
writeEarlyHints(hints: Record<string, string | string[]>, callback?: () => void) {
|
|
208
|
+
if (typeof this.#nodeResponse?.writeEarlyHints === 'function') this.#nodeResponse.writeEarlyHints(hints, callback);
|
|
209
|
+
else callback?.();
|
|
210
|
+
}
|
|
211
|
+
// Trailers need chunked encoding on the wire, which Harper's response layer owns; dropping one silently (a Digest, say) is worse than failing.
|
|
212
|
+
addTrailers(): never {
|
|
213
|
+
throw new UnsupportedResponseMethodError('addTrailers');
|
|
214
|
+
}
|
|
215
|
+
assignSocket(): never {
|
|
216
|
+
throw new UnsupportedResponseMethodError('assignSocket');
|
|
217
|
+
}
|
|
218
|
+
detachSocket(): never {
|
|
219
|
+
throw new UnsupportedResponseMethodError('detachSocket');
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import type { IncomingMessage as NodeIncomingMessage, ServerResponse as NodeServerResponse } from 'node:http';
|
|
2
2
|
import type { Socket } from 'node:net';
|
|
3
3
|
import { TLSSocket } from 'node:tls';
|
|
4
|
-
import {
|
|
5
|
-
import { Readable, PassThrough } from 'node:stream';
|
|
4
|
+
import { Readable } from 'node:stream';
|
|
6
5
|
import { Headers as ResponseHeaders } from './Headers.ts';
|
|
6
|
+
import { NodeAdapterResponse, type AdaptedResponse } from './NodeAdapterResponse.ts';
|
|
7
7
|
import type { ConnectionInfo } from './proxyProtocol.ts';
|
|
8
|
+
import harperLogger from '../../utility/logging/harper_logger.ts';
|
|
8
9
|
|
|
9
10
|
export const isBun = typeof globalThis.Bun !== 'undefined';
|
|
10
11
|
|
|
@@ -178,10 +179,8 @@ export class Request {
|
|
|
178
179
|
* status, headers, and body, resolving the returned promise as soon as headers are available with a
|
|
179
180
|
* streaming body that can be piped back through the Harper middleware chain.
|
|
180
181
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* after headers are sent, the body stream is destroyed with an error — without a listener, Node.js
|
|
184
|
-
* will throw an uncaught exception.
|
|
182
|
+
* Consume the returned body with `pipeline()`, `finished()` or async iteration so stored stream errors
|
|
183
|
+
* and premature closes are reported.
|
|
185
184
|
*
|
|
186
185
|
* Example:
|
|
187
186
|
* server.http((request, next) =>
|
|
@@ -190,11 +189,13 @@ export class Request {
|
|
|
190
189
|
*/
|
|
191
190
|
withNodeAdapter(
|
|
192
191
|
handler: (request: NodeIncomingMessage, response: NodeServerResponse) => void | Promise<void>
|
|
193
|
-
): Promise<
|
|
194
|
-
|
|
195
|
-
const reqHeaders: Record<string, string | string[]> = Object.create(null);
|
|
192
|
+
): Promise<AdaptedResponse> {
|
|
193
|
+
const reqHeaders: Record<string, string | string[]> = {};
|
|
196
194
|
for (const [key, value] of this.headers) {
|
|
197
|
-
|
|
195
|
+
const lowerKey = key.toLowerCase();
|
|
196
|
+
if (lowerKey === '__proto__')
|
|
197
|
+
Object.defineProperty(reqHeaders, lowerKey, { value, configurable: true, enumerable: true, writable: true });
|
|
198
|
+
else reqHeaders[lowerKey] = value;
|
|
198
199
|
}
|
|
199
200
|
|
|
200
201
|
// Proxy the underlying IncomingMessage so body streaming works, but expose
|
|
@@ -209,131 +210,32 @@ export class Request {
|
|
|
209
210
|
},
|
|
210
211
|
}) as NodeIncomingMessage;
|
|
211
212
|
|
|
212
|
-
let
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
resolveResponse = resolve;
|
|
216
|
-
rejectResponse = reject;
|
|
213
|
+
let nodeRes!: NodeAdapterResponse;
|
|
214
|
+
const response = new Promise<AdaptedResponse>((resolve, reject) => {
|
|
215
|
+
nodeRes = new NodeAdapterResponse(nodeReq, this._nodeResponse, resolve, reject);
|
|
217
216
|
});
|
|
218
217
|
|
|
219
|
-
const
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
|
|
218
|
+
const signal = this.signal;
|
|
219
|
+
const onAbort = () => nodeRes.destroy(nodeRes.headersSent ? undefined : signal.reason);
|
|
220
|
+
if (signal.aborted) onAbort();
|
|
221
|
+
else {
|
|
222
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
223
|
+
nodeRes.once('close', () => signal.removeEventListener('abort', onAbort));
|
|
224
|
+
}
|
|
223
225
|
|
|
224
|
-
const
|
|
225
|
-
if (!
|
|
226
|
-
|
|
227
|
-
nodeRes.headersSent = true;
|
|
228
|
-
resolveResponse({ status: nodeRes.statusCode as number, headers: capturedHeaders, body: responseBody });
|
|
229
|
-
}
|
|
226
|
+
const onHandlerFailure = (error: Error) => {
|
|
227
|
+
if (!nodeRes.writableEnded) nodeRes.destroy(error);
|
|
228
|
+
else harperLogger.warn('withNodeAdapter handler failed after ending its response', error);
|
|
230
229
|
};
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
for (const [k, v] of Object.entries(hdrs)) capturedHeaders.set(k, v as string);
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
|
|
244
|
-
nodeRes = Object.assign(new EventEmitter(), {
|
|
245
|
-
statusCode: 200 as number,
|
|
246
|
-
statusMessage: '',
|
|
247
|
-
headersSent: false,
|
|
248
|
-
writable: true,
|
|
249
|
-
writableEnded: false,
|
|
250
|
-
writableFinished: false,
|
|
251
|
-
socket: this._nodeRequest.socket,
|
|
252
|
-
|
|
253
|
-
setHeader(name: string, value: string | number | string[]) {
|
|
254
|
-
if (Array.isArray(value)) {
|
|
255
|
-
// Use set() for first value (overwrites any existing entry) then append() for
|
|
256
|
-
// the rest so multiple values — critical for Set-Cookie — are preserved as an
|
|
257
|
-
// array rather than collapsed into a single comma-joined string.
|
|
258
|
-
if (value.length > 0) {
|
|
259
|
-
capturedHeaders.set(name, String(value[0]));
|
|
260
|
-
for (let i = 1; i < value.length; i++) capturedHeaders.append(name, String(value[i]));
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
capturedHeaders.set(name, String(value));
|
|
264
|
-
}
|
|
265
|
-
return nodeRes;
|
|
266
|
-
},
|
|
267
|
-
getHeader(name: string) {
|
|
268
|
-
return capturedHeaders.get(name);
|
|
269
|
-
},
|
|
270
|
-
getHeaders() {
|
|
271
|
-
return Object.fromEntries(capturedHeaders);
|
|
272
|
-
},
|
|
273
|
-
hasHeader(name: string) {
|
|
274
|
-
return capturedHeaders.has(name);
|
|
275
|
-
},
|
|
276
|
-
removeHeader(name: string) {
|
|
277
|
-
capturedHeaders.delete(name);
|
|
278
|
-
},
|
|
279
|
-
flushHeaders() {
|
|
280
|
-
flushHeaders();
|
|
281
|
-
},
|
|
282
|
-
writeHead(statusCode: number, statusMessageOrHeaders?: string | object, maybeHeaders?: object) {
|
|
283
|
-
if (headersFlushed) return nodeRes;
|
|
284
|
-
nodeRes.statusCode = statusCode;
|
|
285
|
-
const hdrs = typeof statusMessageOrHeaders === 'string' ? maybeHeaders : statusMessageOrHeaders;
|
|
286
|
-
if (hdrs) applyHeaders(hdrs as object | unknown[]);
|
|
287
|
-
flushHeaders();
|
|
288
|
-
return nodeRes;
|
|
289
|
-
},
|
|
290
|
-
write(
|
|
291
|
-
chunk: unknown,
|
|
292
|
-
encoding?: BufferEncoding | ((error?: Error | null) => void),
|
|
293
|
-
callback?: (error?: Error | null) => void
|
|
294
|
-
) {
|
|
295
|
-
flushHeaders();
|
|
296
|
-
if (typeof encoding === 'function') return responseBody.write(chunk as any, encoding);
|
|
297
|
-
return responseBody.write(chunk as any, encoding, callback);
|
|
298
|
-
},
|
|
299
|
-
end(chunk?: unknown, encoding?: BufferEncoding | (() => void), callback?: () => void) {
|
|
300
|
-
flushHeaders();
|
|
301
|
-
nodeRes.writableEnded = true;
|
|
302
|
-
if (typeof chunk === 'function') responseBody.end(chunk as () => void);
|
|
303
|
-
else if (typeof encoding === 'function') responseBody.end(chunk as any, encoding);
|
|
304
|
-
else responseBody.end(chunk as any, encoding, callback);
|
|
305
|
-
return nodeRes;
|
|
306
|
-
},
|
|
307
|
-
destroy(error?: Error) {
|
|
308
|
-
if (!headersFlushed) {
|
|
309
|
-
if (error) rejectResponse(error);
|
|
310
|
-
else rejectResponse(new Error('Response destroyed before headers were sent'));
|
|
311
|
-
// The error has been forwarded to the response promise; suppress the
|
|
312
|
-
// PassThrough 'error' event for this one destroy call so Node doesn't
|
|
313
|
-
// throw due to having no other listeners.
|
|
314
|
-
responseBody.once('error', () => {});
|
|
315
|
-
}
|
|
316
|
-
responseBody.destroy(error);
|
|
317
|
-
return nodeRes;
|
|
318
|
-
},
|
|
319
|
-
}) as unknown as NodeServerResponse;
|
|
320
|
-
|
|
321
|
-
responseBody.on('finish', () => {
|
|
322
|
-
nodeRes.writableFinished = true;
|
|
323
|
-
(nodeRes as unknown as EventEmitter).emit('finish');
|
|
324
|
-
});
|
|
325
|
-
responseBody.on('drain', () => {
|
|
326
|
-
(nodeRes as unknown as EventEmitter).emit('drain');
|
|
327
|
-
});
|
|
328
|
-
responseBody.on('close', () => {
|
|
329
|
-
(nodeRes as unknown as EventEmitter).emit('close');
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
const handlerResult = handler(nodeReq, nodeRes);
|
|
333
|
-
if (handlerResult != null && typeof (handlerResult as unknown as Promise<void>).then === 'function') {
|
|
334
|
-
(handlerResult as unknown as Promise<void>).catch((err: unknown) => {
|
|
335
|
-
if (!headersFlushed) rejectResponse(err);
|
|
336
|
-
});
|
|
230
|
+
let handlerResult: void | Promise<void>;
|
|
231
|
+
try {
|
|
232
|
+
handlerResult = handler(nodeReq, nodeRes);
|
|
233
|
+
} catch (error) {
|
|
234
|
+
onHandlerFailure(error as Error);
|
|
235
|
+
return response;
|
|
236
|
+
}
|
|
237
|
+
if (typeof (handlerResult as Promise<void>)?.then === 'function') {
|
|
238
|
+
Promise.resolve(handlerResult).catch(onHandlerFailure);
|
|
337
239
|
}
|
|
338
240
|
return response;
|
|
339
241
|
}
|