@webpieces/http-client-node 0.4.690 → 0.4.692
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -5
- package/src/NodeProxyClient.d.ts +36 -1
- package/src/NodeProxyClient.js +46 -0
- package/src/NodeProxyClient.js.map +1 -1
- package/src/index.d.ts +1 -1
- package/src/index.js +2 -1
- package/src/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-client-node",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.692",
|
|
4
4
|
"description": "Server-side HTTP client for webpieces: inversify-wired, reads RequestContext directly, mints OIDC/shared-secret delivery auth, resolves Cloud Run URLs from a service name",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"access": "public"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@webpieces/core-context": "0.4.
|
|
26
|
-
"@webpieces/core-util": "0.4.
|
|
27
|
-
"@webpieces/gcp-identity": "0.4.
|
|
28
|
-
"@webpieces/http-client-core": "0.4.
|
|
25
|
+
"@webpieces/core-context": "0.4.692",
|
|
26
|
+
"@webpieces/core-util": "0.4.692",
|
|
27
|
+
"@webpieces/gcp-identity": "0.4.692",
|
|
28
|
+
"@webpieces/http-client-core": "0.4.692",
|
|
29
29
|
"inversify": "7.10.4",
|
|
30
30
|
"reflect-metadata": "0.2.2"
|
|
31
31
|
}
|
package/src/NodeProxyClient.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AuthMeta, DestinationTrust, RouteMetadata, Secrets } from '@webpieces/core-util';
|
|
2
2
|
import { RequestContextHeaders } from '@webpieces/core-context';
|
|
3
3
|
import { GcpOidc } from '@webpieces/gcp-identity';
|
|
4
|
-
import { ApiPrototype, ProxyClient } from '@webpieces/http-client-core';
|
|
4
|
+
import { ApiPrototype, ProxyClient, TranslatedFailure } from '@webpieces/http-client-core';
|
|
5
5
|
import { ClientConfig } from './ClientConfig';
|
|
6
6
|
/**
|
|
7
7
|
* The server-side {@link ProxyClient}. Everything a browser cannot do lives here: reading the
|
|
@@ -61,6 +61,41 @@ export declare class NodeProxyClient extends ProxyClient {
|
|
|
61
61
|
private recordCall;
|
|
62
62
|
/** A server can satisfy every auth mode, so nothing is rejected at bind time. */
|
|
63
63
|
protected assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void;
|
|
64
|
+
/**
|
|
65
|
+
* SERVER-TO-SERVER: a 4xx received from a dependency becomes THIS server's own 500.
|
|
66
|
+
*
|
|
67
|
+
* THE INVARIANT:
|
|
68
|
+
*
|
|
69
|
+
* A status received from a downstream dependency describes OUR request to it. It is never the
|
|
70
|
+
* status we return to OUR caller. The server that answered 404 is correct; the server that
|
|
71
|
+
* asked for a route that does not exist is broken, and must say so as a 500.
|
|
72
|
+
*
|
|
73
|
+
* Every 4xx is a CALLER-side defect on this hop: 404 = wrong path / wrong base URL / a dependency
|
|
74
|
+
* that is not deployed yet, 400 = we sent a malformed request, 401/403 = our service credentials
|
|
75
|
+
* or the callee's caller allow-list are wrong. None of them is an answer for whoever called US, and
|
|
76
|
+
* relaying one lets an internal misconfiguration impersonate a legitimate response. That is not
|
|
77
|
+
* hypothetical: a partner-facing Management API reported an EMPTY store estate for an org with six
|
|
78
|
+
* live storefronts, because its dependency had not been promoted and Express served an HTML 404
|
|
79
|
+
* which arrived here as `HttpNotFoundError` and went straight back out. A 500 would have been
|
|
80
|
+
* loud, correct, and attributable to the one server that actually had the bug — which is the whole
|
|
81
|
+
* point: only ONE server should be paged for this.
|
|
82
|
+
*
|
|
83
|
+
* DELIBERATELY 4xx ONLY. 5xx (502/503/504) already mean "the dependency is unavailable", which is
|
|
84
|
+
* honest and useful outward, and 500 is already a 500. `HttpUserError` (266, a 2xx code carrying
|
|
85
|
+
* user validation) and `HttpVendorError` (598) are not statuses about our request at all. All of
|
|
86
|
+
* them pass through untouched.
|
|
87
|
+
*
|
|
88
|
+
* THE OPT-OUT IS `appRegistered`, not a config key. A thin proxy or gateway that genuinely wants to
|
|
89
|
+
* relay a downstream status as its own registers a `ClientRegistry` error translation for it at
|
|
90
|
+
* startup — one greppable line saying so out loud — and that translation wins here. Only the
|
|
91
|
+
* framework's built-in default mapping gets wrapped. There is no flag, because a flag would make
|
|
92
|
+
* the dangerous choice invisible in the code that suffers from it.
|
|
93
|
+
*
|
|
94
|
+
* The downstream diagnostic is NOT lost: the original error (which for the incident above names the
|
|
95
|
+
* method, the status, the `text/html` content-type and a snippet of the body) is both quoted in the
|
|
96
|
+
* message and kept as `httpCause`.
|
|
97
|
+
*/
|
|
98
|
+
protected adaptDownstreamFailure(failure: TranslatedFailure, callId: string): Error;
|
|
64
99
|
}
|
|
65
100
|
/**
|
|
66
101
|
* DI token for the `Provider<NodeProxyClient>` that hands out RPC clients — one per API contract.
|
package/src/NodeProxyClient.js
CHANGED
|
@@ -121,6 +121,52 @@ let NodeProxyClient = class NodeProxyClient extends http_client_core_1.ProxyClie
|
|
|
121
121
|
}
|
|
122
122
|
/** A server can satisfy every auth mode, so nothing is rejected at bind time. */
|
|
123
123
|
assertEndpointSupported(_authMeta, _methodName) { }
|
|
124
|
+
/**
|
|
125
|
+
* SERVER-TO-SERVER: a 4xx received from a dependency becomes THIS server's own 500.
|
|
126
|
+
*
|
|
127
|
+
* THE INVARIANT:
|
|
128
|
+
*
|
|
129
|
+
* A status received from a downstream dependency describes OUR request to it. It is never the
|
|
130
|
+
* status we return to OUR caller. The server that answered 404 is correct; the server that
|
|
131
|
+
* asked for a route that does not exist is broken, and must say so as a 500.
|
|
132
|
+
*
|
|
133
|
+
* Every 4xx is a CALLER-side defect on this hop: 404 = wrong path / wrong base URL / a dependency
|
|
134
|
+
* that is not deployed yet, 400 = we sent a malformed request, 401/403 = our service credentials
|
|
135
|
+
* or the callee's caller allow-list are wrong. None of them is an answer for whoever called US, and
|
|
136
|
+
* relaying one lets an internal misconfiguration impersonate a legitimate response. That is not
|
|
137
|
+
* hypothetical: a partner-facing Management API reported an EMPTY store estate for an org with six
|
|
138
|
+
* live storefronts, because its dependency had not been promoted and Express served an HTML 404
|
|
139
|
+
* which arrived here as `HttpNotFoundError` and went straight back out. A 500 would have been
|
|
140
|
+
* loud, correct, and attributable to the one server that actually had the bug — which is the whole
|
|
141
|
+
* point: only ONE server should be paged for this.
|
|
142
|
+
*
|
|
143
|
+
* DELIBERATELY 4xx ONLY. 5xx (502/503/504) already mean "the dependency is unavailable", which is
|
|
144
|
+
* honest and useful outward, and 500 is already a 500. `HttpUserError` (266, a 2xx code carrying
|
|
145
|
+
* user validation) and `HttpVendorError` (598) are not statuses about our request at all. All of
|
|
146
|
+
* them pass through untouched.
|
|
147
|
+
*
|
|
148
|
+
* THE OPT-OUT IS `appRegistered`, not a config key. A thin proxy or gateway that genuinely wants to
|
|
149
|
+
* relay a downstream status as its own registers a `ClientRegistry` error translation for it at
|
|
150
|
+
* startup — one greppable line saying so out loud — and that translation wins here. Only the
|
|
151
|
+
* framework's built-in default mapping gets wrapped. There is no flag, because a flag would make
|
|
152
|
+
* the dangerous choice invisible in the code that suffers from it.
|
|
153
|
+
*
|
|
154
|
+
* The downstream diagnostic is NOT lost: the original error (which for the incident above names the
|
|
155
|
+
* method, the status, the `text/html` content-type and a snippet of the body) is both quoted in the
|
|
156
|
+
* message and kept as `httpCause`.
|
|
157
|
+
*/
|
|
158
|
+
adaptDownstreamFailure(failure, callId) {
|
|
159
|
+
if (failure.appRegistered) {
|
|
160
|
+
return failure.error;
|
|
161
|
+
}
|
|
162
|
+
if (failure.statusCode < 400 || failure.statusCode >= 500) {
|
|
163
|
+
return failure.error;
|
|
164
|
+
}
|
|
165
|
+
return new core_util_1.HttpInternalServerError(`${callId}: dependency answered HTTP ${failure.statusCode}. That status describes OUR ` +
|
|
166
|
+
`request to it, not an answer for our caller, so this server owns it as a 500 — check the ` +
|
|
167
|
+
`path, the base URL, whether the dependency is deployed, and our service credentials. ` +
|
|
168
|
+
`Downstream said: ${failure.error.message}`, failure.error);
|
|
169
|
+
}
|
|
124
170
|
};
|
|
125
171
|
exports.NodeProxyClient = NodeProxyClient;
|
|
126
172
|
exports.NodeProxyClient = NodeProxyClient = tslib_1.__decorate([
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"NodeProxyClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/NodeProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport {\n AuthMeta,\n ClientRegistry,\n DestinationTrust,\n RecordedEndpoint,\n RecordedError,\n RouteMetadata,\n Secrets,\n SECRETS,\n TestCaseRecorder,\n toError,\n} from '@webpieces/core-util';\nimport { RequestContext, RequestContextHeaders, provideFrameworkTransient } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport { ApiPrototype, ProxyClient } from '@webpieces/http-client-core';\nimport { ClientConfig } from './ClientConfig';\n\n/**\n * The server-side {@link ProxyClient}. Everything a browser cannot do lives here: reading the\n * ambient RequestContext, minting OIDC tokens, holding shared secrets, and recording test cases.\n *\n * TRANSIENT on purpose. Every `createRpcClient(api, config)` needs its own instance, because `init()`\n * binds one instance to exactly one API contract and one target. {@link ProxyClientProvider} hands\n * them out — see its doc.\n */\n@provideFrameworkTransient()\nexport class NodeProxyClient extends ProxyClient {\n private config!: ClientConfig;\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(RequestContextHeaders) private readonly headers: RequestContextHeaders,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n // @optional: only @AuthSharedSecret endpoints need it; the client sends its bound value.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional() @inject(SECRETS) private readonly secrets?: Secrets,\n ) {\n super();\n }\n\n /** Bind this client to one API contract + target. */\n init(apiPrototype: ApiPrototype<object>, config: ClientConfig): void {\n this.config = config;\n this.initRoutes(apiPrototype);\n }\n\n /**\n * The same chain every client runs — a ClientRegistry mapping, else the installed deriver — but\n * with NODE's fallback: THROW. A server has no \"own origin\" to fall back to the way a browser\n * does, so an unresolvable peer is a setup bug and must fail loudly (the error names the fixes).\n *\n * Resolved per call, never at construction, so building a client stays synchronous. Any metadata\n * read beneath a deriver is memoized process-wide, so only the first call pays.\n */\n protected override resolveBaseUrl(): Promise<string> {\n return ClientRegistry.resolve(this.config.svcName);\n }\n\n /**\n * Straight from the RequestContext. Throws when there is no active request scope.\n *\n * `destination` rides through unchanged: this is the ONE client that can legitimately propagate a\n * verified identity, and it does so exactly when the callee will authenticate us (@AuthOidc /\n * @AuthSharedSecret). Calling a peer's @Public or @AuthJwt endpoint now omits `x-user-id` and\n * friends instead of shipping headers that endpoint's AuthFilter is obliged to reject.\n */\n protected override outboundContextHeaders(destination: DestinationTrust): Map<string, string> {\n return this.headers.buildOutboundHeaders(destination);\n }\n\n /**\n * Attach the outbound credential for the endpoint's AuthMode: an @AuthOidc bearer minted as\n * this caller's runtime SA (audience = the callee base URL — the server verifies the signature\n * + caller allow-list), or the @AuthSharedSecret(key) value THIS client sends from its bound\n * {@link Secrets}. Both ride in the ONE `Authorization` header under their own scheme —\n * `Bearer <oidc>` / `Webpieces <secret>` — which is never a context key, so it cannot leak onto\n * the next hop. Never reads process.env.\n */\n protected override async attachOutboundAuth(\n route: RouteMetadata,\n baseUrl: string,\n httpHeaders: Record<string, string>,\n ): Promise<void> {\n const mode = route.authMeta?.mode;\n if (mode?.kind === 'oidc') {\n httpHeaders['Authorization'] = `Bearer ${await this.gcpOidc.mintIdToken(baseUrl)}`;\n } else if (mode?.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (!secret) {\n throw new Error(\n `No shared secret configured for @AuthSharedSecret('${mode.secretKey}') endpoint ${route.methodName}`,\n );\n }\n // Same header as a JWT/OIDC token, but its OWN scheme, so a secret can never be\n // mistaken for a token nor accepted where one was expected.\n httpHeaders['Authorization'] = `Webpieces ${secret}`;\n }\n }\n\n /**\n * Test-case recording hook (mirror of Java HttpsJsonClientInvokeHandler): if a recorder is\n * travelling in the magic context, capture this outbound call + its result so it becomes a mock\n * in the generated test. Absent a recorder this is exactly the base behavior.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n protected override async execute(\n route: RouteMetadata,\n requestDto: unknown,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n method: () => Promise<unknown>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n ): Promise<unknown> {\n const recorder = this.headers.findRecorder();\n if (!recorder) {\n return super.execute(route, requestDto, method);\n }\n return this.recordCall(recorder, route, requestDto, method);\n }\n\n /**\n * Execute the call while recording it (args + masked ctx snapshot + result).\n *\n * The snapshot is a FIXTURE field, not a log line, so it is built here rather than handed down\n * from the call path — a logging backend stamps its own fields and never sees this.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n private async recordCall(\n recorder: TestCaseRecorder,\n route: RouteMetadata,\n requestDto: unknown,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n method: () => Promise<unknown>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n ): Promise<unknown> {\n const ctxSnapshot: Record<string, string> = {};\n for (const entry of RequestContext.buildLogFields().entries()) {\n ctxSnapshot[entry[0]] = entry[1];\n }\n const recorded = new RecordedEndpoint(this.contractName(), route.methodName, [requestDto], ctxSnapshot);\n recorder.addEndpointInfo(recorded);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- capture failure into the recording, then rethrow unchanged\n try {\n const response = await super.execute(route, requestDto, method);\n recorded.successResponse = response;\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n recorded.failureResponse = new RecordedError(error.name, error.message);\n throw err;\n }\n }\n\n /** A server can satisfy every auth mode, so nothing is rejected at bind time. */\n protected override assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void {}\n}\n\n/**\n * DI token for the `Provider<NodeProxyClient>` that hands out RPC clients — one per API contract.\n * `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.\n *\n * Because NodeProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound\n * `@provideFrameworkSingleton`, the very same Provider would instead hand back one lazily-created\n * instance — the provider caches nothing, so the target's scope decides.)\n */\n// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T\nexport const NODE_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<NodeProxyClient>');\n"]}
|
|
1
|
+
{"version":3,"file":"NodeProxyClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/NodeProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import { inject, optional } from 'inversify';\nimport {\n AuthMeta,\n ClientRegistry,\n DestinationTrust,\n HttpInternalServerError,\n RecordedEndpoint,\n RecordedError,\n RouteMetadata,\n Secrets,\n SECRETS,\n TestCaseRecorder,\n toError,\n} from '@webpieces/core-util';\nimport { RequestContext, RequestContextHeaders, provideFrameworkTransient } from '@webpieces/core-context';\nimport { GcpOidc } from '@webpieces/gcp-identity';\nimport { ApiPrototype, ProxyClient, TranslatedFailure } from '@webpieces/http-client-core';\nimport { ClientConfig } from './ClientConfig';\n\n/**\n * The server-side {@link ProxyClient}. Everything a browser cannot do lives here: reading the\n * ambient RequestContext, minting OIDC tokens, holding shared secrets, and recording test cases.\n *\n * TRANSIENT on purpose. Every `createRpcClient(api, config)` needs its own instance, because `init()`\n * binds one instance to exactly one API contract and one target. {@link ProxyClientProvider} hands\n * them out — see its doc.\n */\n@provideFrameworkTransient()\nexport class NodeProxyClient extends ProxyClient {\n private config!: ClientConfig;\n\n constructor(\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(RequestContextHeaders) private readonly headers: RequestContextHeaders,\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @inject(GcpOidc) private readonly gcpOidc: GcpOidc,\n // @optional: only @AuthSharedSecret endpoints need it; the client sends its bound value.\n // webpieces-disable inject-annotation-not-needed-for-concrete-class -- DI-resolved param; the esbuild/vitest path elides type-only imports (no design:paramtypes), so the explicit token is required\n @optional() @inject(SECRETS) private readonly secrets?: Secrets,\n ) {\n super();\n }\n\n /** Bind this client to one API contract + target. */\n init(apiPrototype: ApiPrototype<object>, config: ClientConfig): void {\n this.config = config;\n this.initRoutes(apiPrototype);\n }\n\n /**\n * The same chain every client runs — a ClientRegistry mapping, else the installed deriver — but\n * with NODE's fallback: THROW. A server has no \"own origin\" to fall back to the way a browser\n * does, so an unresolvable peer is a setup bug and must fail loudly (the error names the fixes).\n *\n * Resolved per call, never at construction, so building a client stays synchronous. Any metadata\n * read beneath a deriver is memoized process-wide, so only the first call pays.\n */\n protected override resolveBaseUrl(): Promise<string> {\n return ClientRegistry.resolve(this.config.svcName);\n }\n\n /**\n * Straight from the RequestContext. Throws when there is no active request scope.\n *\n * `destination` rides through unchanged: this is the ONE client that can legitimately propagate a\n * verified identity, and it does so exactly when the callee will authenticate us (@AuthOidc /\n * @AuthSharedSecret). Calling a peer's @Public or @AuthJwt endpoint now omits `x-user-id` and\n * friends instead of shipping headers that endpoint's AuthFilter is obliged to reject.\n */\n protected override outboundContextHeaders(destination: DestinationTrust): Map<string, string> {\n return this.headers.buildOutboundHeaders(destination);\n }\n\n /**\n * Attach the outbound credential for the endpoint's AuthMode: an @AuthOidc bearer minted as\n * this caller's runtime SA (audience = the callee base URL — the server verifies the signature\n * + caller allow-list), or the @AuthSharedSecret(key) value THIS client sends from its bound\n * {@link Secrets}. Both ride in the ONE `Authorization` header under their own scheme —\n * `Bearer <oidc>` / `Webpieces <secret>` — which is never a context key, so it cannot leak onto\n * the next hop. Never reads process.env.\n */\n protected override async attachOutboundAuth(\n route: RouteMetadata,\n baseUrl: string,\n httpHeaders: Record<string, string>,\n ): Promise<void> {\n const mode = route.authMeta?.mode;\n if (mode?.kind === 'oidc') {\n httpHeaders['Authorization'] = `Bearer ${await this.gcpOidc.mintIdToken(baseUrl)}`;\n } else if (mode?.kind === 'shared-secret') {\n const secret = this.secrets?.get(mode.secretKey);\n if (!secret) {\n throw new Error(\n `No shared secret configured for @AuthSharedSecret('${mode.secretKey}') endpoint ${route.methodName}`,\n );\n }\n // Same header as a JWT/OIDC token, but its OWN scheme, so a secret can never be\n // mistaken for a token nor accepted where one was expected.\n httpHeaders['Authorization'] = `Webpieces ${secret}`;\n }\n }\n\n /**\n * Test-case recording hook (mirror of Java HttpsJsonClientInvokeHandler): if a recorder is\n * travelling in the magic context, capture this outbound call + its result so it becomes a mock\n * in the generated test. Absent a recorder this is exactly the base behavior.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n protected override async execute(\n route: RouteMetadata,\n requestDto: unknown,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n method: () => Promise<unknown>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n ): Promise<unknown> {\n const recorder = this.headers.findRecorder();\n if (!recorder) {\n return super.execute(route, requestDto, method);\n }\n return this.recordCall(recorder, route, requestDto, method);\n }\n\n /**\n * Execute the call while recording it (args + masked ctx snapshot + result).\n *\n * The snapshot is a FIXTURE field, not a log line, so it is built here rather than handed down\n * from the call path — a logging backend stamps its own fields and never sees this.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n private async recordCall(\n recorder: TestCaseRecorder,\n route: RouteMetadata,\n requestDto: unknown,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n method: () => Promise<unknown>,\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n ): Promise<unknown> {\n const ctxSnapshot: Record<string, string> = {};\n for (const entry of RequestContext.buildLogFields().entries()) {\n ctxSnapshot[entry[0]] = entry[1];\n }\n const recorded = new RecordedEndpoint(this.contractName(), route.methodName, [requestDto], ctxSnapshot);\n recorder.addEndpointInfo(recorded);\n\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions -- capture failure into the recording, then rethrow unchanged\n try {\n const response = await super.execute(route, requestDto, method);\n recorded.successResponse = response;\n return response;\n } catch (err: unknown) {\n const error = toError(err);\n recorded.failureResponse = new RecordedError(error.name, error.message);\n throw err;\n }\n }\n\n /** A server can satisfy every auth mode, so nothing is rejected at bind time. */\n protected override assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void {}\n\n /**\n * SERVER-TO-SERVER: a 4xx received from a dependency becomes THIS server's own 500.\n *\n * THE INVARIANT:\n *\n * A status received from a downstream dependency describes OUR request to it. It is never the\n * status we return to OUR caller. The server that answered 404 is correct; the server that\n * asked for a route that does not exist is broken, and must say so as a 500.\n *\n * Every 4xx is a CALLER-side defect on this hop: 404 = wrong path / wrong base URL / a dependency\n * that is not deployed yet, 400 = we sent a malformed request, 401/403 = our service credentials\n * or the callee's caller allow-list are wrong. None of them is an answer for whoever called US, and\n * relaying one lets an internal misconfiguration impersonate a legitimate response. That is not\n * hypothetical: a partner-facing Management API reported an EMPTY store estate for an org with six\n * live storefronts, because its dependency had not been promoted and Express served an HTML 404\n * which arrived here as `HttpNotFoundError` and went straight back out. A 500 would have been\n * loud, correct, and attributable to the one server that actually had the bug — which is the whole\n * point: only ONE server should be paged for this.\n *\n * DELIBERATELY 4xx ONLY. 5xx (502/503/504) already mean \"the dependency is unavailable\", which is\n * honest and useful outward, and 500 is already a 500. `HttpUserError` (266, a 2xx code carrying\n * user validation) and `HttpVendorError` (598) are not statuses about our request at all. All of\n * them pass through untouched.\n *\n * THE OPT-OUT IS `appRegistered`, not a config key. A thin proxy or gateway that genuinely wants to\n * relay a downstream status as its own registers a `ClientRegistry` error translation for it at\n * startup — one greppable line saying so out loud — and that translation wins here. Only the\n * framework's built-in default mapping gets wrapped. There is no flag, because a flag would make\n * the dangerous choice invisible in the code that suffers from it.\n *\n * The downstream diagnostic is NOT lost: the original error (which for the incident above names the\n * method, the status, the `text/html` content-type and a snippet of the body) is both quoted in the\n * message and kept as `httpCause`.\n */\n protected override adaptDownstreamFailure(failure: TranslatedFailure, callId: string): Error {\n if (failure.appRegistered) {\n return failure.error;\n }\n if (failure.statusCode < 400 || failure.statusCode >= 500) {\n return failure.error;\n }\n return new HttpInternalServerError(\n `${callId}: dependency answered HTTP ${failure.statusCode}. That status describes OUR ` +\n `request to it, not an answer for our caller, so this server owns it as a 500 — check the ` +\n `path, the base URL, whether the dependency is deployed, and our service credentials. ` +\n `Downstream said: ${failure.error.message}`,\n failure.error,\n );\n }\n}\n\n/**\n * DI token for the `Provider<NodeProxyClient>` that hands out RPC clients — one per API contract.\n * `Provider<T>` is erased at runtime, so it cannot be its own token; this Symbol names T.\n *\n * Because NodeProxyClient is bound TRANSIENT, every `get()` constructs a new one. (Were it bound\n * `@provideFrameworkSingleton`, the very same Provider would instead hand back one lazily-created\n * instance — the provider caches nothing, so the target's scope decides.)\n */\n// webpieces-disable no-symbol-di-tokens -- Provider<T> is erased at runtime; the Symbol names T\nexport const NODE_PROXY_CLIENT_PROVIDER = Symbol.for('Provider<NodeProxyClient>');\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -21,5 +21,5 @@
|
|
|
21
21
|
export { ClientHttpFactory } from './ClientHttpFactory';
|
|
22
22
|
export { NodeProxyClient, NODE_PROXY_CLIENT_PROVIDER } from './NodeProxyClient';
|
|
23
23
|
export { ClientConfig } from './ClientConfig';
|
|
24
|
-
export { ProxyClient, ClientErrorTranslator } from '@webpieces/http-client-core';
|
|
24
|
+
export { ProxyClient, ClientErrorTranslator, TranslatedFailure } from '@webpieces/http-client-core';
|
|
25
25
|
export type { ApiPrototype } from '@webpieces/http-client-core';
|
package/src/index.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* ```
|
|
21
21
|
*/
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.ClientErrorTranslator = exports.ProxyClient = exports.ClientConfig = exports.NODE_PROXY_CLIENT_PROVIDER = exports.NodeProxyClient = exports.ClientHttpFactory = void 0;
|
|
23
|
+
exports.TranslatedFailure = exports.ClientErrorTranslator = exports.ProxyClient = exports.ClientConfig = exports.NODE_PROXY_CLIENT_PROVIDER = exports.NodeProxyClient = exports.ClientHttpFactory = void 0;
|
|
24
24
|
var ClientHttpFactory_1 = require("./ClientHttpFactory");
|
|
25
25
|
Object.defineProperty(exports, "ClientHttpFactory", { enumerable: true, get: function () { return ClientHttpFactory_1.ClientHttpFactory; } });
|
|
26
26
|
var NodeProxyClient_1 = require("./NodeProxyClient");
|
|
@@ -32,4 +32,5 @@ Object.defineProperty(exports, "ClientConfig", { enumerable: true, get: function
|
|
|
32
32
|
var http_client_core_1 = require("@webpieces/http-client-core");
|
|
33
33
|
Object.defineProperty(exports, "ProxyClient", { enumerable: true, get: function () { return http_client_core_1.ProxyClient; } });
|
|
34
34
|
Object.defineProperty(exports, "ClientErrorTranslator", { enumerable: true, get: function () { return http_client_core_1.ClientErrorTranslator; } });
|
|
35
|
+
Object.defineProperty(exports, "TranslatedFailure", { enumerable: true, get: function () { return http_client_core_1.TranslatedFailure; } });
|
|
35
36
|
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;;AAEH,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,qDAAgF;AAAvE,kHAAA,eAAe,OAAA;AAAE,6HAAA,0BAA0B,OAAA;AACpD,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,uEAAuE;AACvE,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-node/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;GAmBG;;;AAEH,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,qDAAgF;AAAvE,kHAAA,eAAe,OAAA;AAAE,6HAAA,0BAA0B,OAAA;AACpD,+CAA8C;AAArC,4GAAA,YAAY,OAAA;AAErB,uEAAuE;AACvE,gEAAoG;AAA3F,+GAAA,WAAW,OAAA;AAAE,yHAAA,qBAAqB,OAAA;AAAE,qHAAA,iBAAiB,OAAA","sourcesContent":["/**\n * @webpieces/http-client-node\n *\n * The SERVER-side HTTP client. Reads an API contract's decorators and generates type-safe HTTP\n * clients from it — the same contract the callee's controller implements.\n *\n * Node-only, so unlike @webpieces/http-client-browser it is fully inversify-wired and reads the\n * magic context straight out of the AsyncLocalStorage-backed RequestContext. There is no\n * ContextReader indirection, because a server has exactly one right answer, and a call made\n * OUTSIDE `RequestContext.run(...)` throws instead of silently dropping the trace.\n *\n * Usage:\n * ```typescript\n * import { ClientHttpFactory, ClientConfig } from '@webpieces/http-client-node';\n *\n * // inject the factory, then one client per contract\n * const server2 = factory.createRpcClient(Server2Api, new ClientConfig('server2'));\n * const response = await server2.fetchValue(req);\n * ```\n */\n\nexport { ClientHttpFactory } from './ClientHttpFactory';\nexport { NodeProxyClient, NODE_PROXY_CLIENT_PROVIDER } from './NodeProxyClient';\nexport { ClientConfig } from './ClientConfig';\n\n// The isomorphic engine, re-exported so a server app needs one import.\nexport { ProxyClient, ClientErrorTranslator, TranslatedFailure } from '@webpieces/http-client-core';\nexport type { ApiPrototype } from '@webpieces/http-client-core';\n"]}
|