@webpieces/http-client-core 0.4.762 → 0.4.765
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 +2 -2
- package/src/ClientErrorTranslator.d.ts +4 -87
- package/src/ClientErrorTranslator.js +39 -133
- package/src/ClientErrorTranslator.js.map +1 -1
- package/src/ProxyClient.d.ts +1 -1
- package/src/ProxyClient.js +1 -1
- package/src/ProxyClient.js.map +1 -1
- package/src/RequestOutcome.d.ts +1 -1
- package/src/RequestOutcome.js +1 -1
- package/src/RequestOutcome.js.map +1 -1
- package/src/ResponseBodyReader.d.ts +7 -7
- package/src/ResponseBodyReader.js +8 -7
- package/src/ResponseBodyReader.js.map +1 -1
- package/src/TranslatedFailure.d.ts +2 -2
- package/src/TranslatedFailure.js +2 -2
- package/src/TranslatedFailure.js.map +1 -1
- package/src/UnexpectedApiResponseError.d.ts +5 -0
- package/src/UnexpectedApiResponseError.js +14 -0
- package/src/UnexpectedApiResponseError.js.map +1 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +3 -1
- package/src/index.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/http-client-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.765",
|
|
4
4
|
"description": "Isomorphic core of the webpieces HTTP client: the decorator-driven ProxyClient, error translation, and the Proxy trap shared by http-client-node and http-client-browser",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -21,6 +21,6 @@
|
|
|
21
21
|
"access": "public"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@webpieces/core-util": "0.4.
|
|
24
|
+
"@webpieces/core-util": "0.4.765"
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -1,93 +1,10 @@
|
|
|
1
1
|
import { HttpResponseDto } from '@webpieces/core-util';
|
|
2
2
|
import { TranslatedFailure } from './TranslatedFailure';
|
|
3
|
-
/**
|
|
4
|
-
* ClientErrorTranslator - Translates HTTP error responses to HttpError exceptions.
|
|
5
|
-
*
|
|
6
|
-
* This is the CLIENT-SIDE reverse of ExpressWrapper.handleError() on the server.
|
|
7
|
-
* It reconstructs typed HttpError exceptions from ProtocolError JSON responses.
|
|
8
|
-
*
|
|
9
|
-
* Architecture:
|
|
10
|
-
* - Server: HttpError → ExpressWrapper.handleError() → an {@link HttpResponseDto} on the wire
|
|
11
|
-
* - Client: that response → ClientErrorTranslator.translateError() → TranslatedFailure
|
|
12
|
-
*
|
|
13
|
-
* Both sides speak {@link HttpResponseDto}, which is what makes an app's `ErrorTranslators` one
|
|
14
|
-
* object with two halves that can be read against each other: `toWire` produces exactly the shape
|
|
15
|
-
* `fromWire` consumes, whether the reader was `http-client-node` or `http-client-browser`.
|
|
16
|
-
*
|
|
17
|
-
* This achieves symmetric error handling - server throws typed exceptions,
|
|
18
|
-
* client receives typed exceptions.
|
|
19
|
-
*
|
|
20
|
-
* The symmetry is in the TYPE and the structured fields, NOT in the prose: the server sends the real
|
|
21
|
-
* `Error.message` for `UserError` alone and a generic reason phrase for everything else. See
|
|
22
|
-
* {@link builtInError} and, on the server, `HttpErrorWireMapper`.
|
|
23
|
-
*
|
|
24
|
-
* It returns a {@link TranslatedFailure} rather than a bare `Error` because the mapping is only HALF
|
|
25
|
-
* the decision. It is ISOMORPHIC — the same mapping runs in a browser and in a server — and the two
|
|
26
|
-
* environments must NOT do the same thing with a downstream 4xx (see
|
|
27
|
-
* `ProxyClient.adaptDownstreamFailure`). The wrapper carries the one fact that hook cannot recover
|
|
28
|
-
* on its own: whether the APP claimed this status, or the built-in default did.
|
|
29
|
-
*/
|
|
3
|
+
/** Reconstructs transport-neutral API failures from an HTTP response. */
|
|
30
4
|
export declare class ClientErrorTranslator {
|
|
31
|
-
/**
|
|
32
|
-
* Parse an error response and decide which error the caller should see, and who decided it.
|
|
33
|
-
*
|
|
34
|
-
* The app's `ErrorTranslators` wins, so an app can reconstruct its OWN error types (e.g. a
|
|
35
|
-
* custom 460) AND override built-ins. `undefined` means "not mine" — fall through to
|
|
36
|
-
* {@link builtInError}, which stays the generic default. Symmetric with the server's
|
|
37
|
-
* ExpressWrapper.handleError(), which consults ClientRegistry.tryTranslateToWire() first.
|
|
38
|
-
*
|
|
39
|
-
* @param response - the WHOLE response, normalised out of the transport by
|
|
40
|
-
* `HttpResponseDtoFactory` — status code, reason phrase, header list and parsed body
|
|
41
|
-
* @returns the chosen error plus its provenance and the downstream status
|
|
42
|
-
*/
|
|
43
5
|
static translateError(response: HttpResponseDto): TranslatedFailure;
|
|
44
|
-
/**
|
|
45
|
-
* The built-in status → error mapping (symmetric with the server's ExpressWrapper.handleError()):
|
|
46
|
-
* - 400 → BadRequestError (with field, guiAlertMessage)
|
|
47
|
-
* - 266 → UserError (with errorCode) - 2xx code for user validation
|
|
48
|
-
* - 401 → UnauthorizedError (with subType)
|
|
49
|
-
* - 403 → ForbiddenError
|
|
50
|
-
* - 404 → NotFoundError
|
|
51
|
-
* - 408 → RequestTimeoutError
|
|
52
|
-
* - 429 → TooManyRequestsError
|
|
53
|
-
* - 500 → InternalError
|
|
54
|
-
* - 502 → BadGatewayError
|
|
55
|
-
* - 503 → ServiceUnavailableError
|
|
56
|
-
* - 504 → GatewayTimeoutError
|
|
57
|
-
* - 598 → VendorError (with waitSeconds) - custom status code
|
|
58
|
-
* - other → generic HttpError
|
|
59
|
-
*
|
|
60
|
-
* # What `message` means on THIS side of the wire
|
|
61
|
-
*
|
|
62
|
-
* The reconstructed error carries whatever text the wire carried, and for every status except
|
|
63
|
-
* **266** a webpieces server deliberately sends only the GENERIC reason phrase — 'Not Found',
|
|
64
|
-
* 'Internal Server Error', … See `HttpErrorWireMapper` (http-server) for why: `Error.message` is
|
|
65
|
-
* an operator-facing field that routinely quotes internal detail, so it stays in the server's log
|
|
66
|
-
* and never reaches a caller. `UserError` (266) is the one type whose message was WRITTEN for
|
|
67
|
-
* a human to read, and it arrives verbatim.
|
|
68
|
-
*
|
|
69
|
-
* So: branch on the TYPE, on `subType`, on `errorCode`, or on `guiAlertMessage` — never on the
|
|
70
|
-
* prose of `message`. It is now a constant per status by design, and treating it as diagnostic
|
|
71
|
-
* information will not work against a current webpieces server. The diagnosis lives in the
|
|
72
|
-
* server's logs, correlated by request id.
|
|
73
|
-
*
|
|
74
|
-
* (An app that publishes richer text on purpose does it through
|
|
75
|
-
* `ClientRegistry.setErrorTranslators()`, which is consulted before this mapping on both sides.)
|
|
76
|
-
*
|
|
77
|
-
* PUBLIC, and the client-side twin of `HttpErrorWireMapper.toResponse` being public on the
|
|
78
|
-
* server: the webpieces DEFAULT is delegable, so an app whose `fromWire` claims one status can
|
|
79
|
-
* hand every other status straight back here instead of copying this ladder.
|
|
80
|
-
*/
|
|
6
|
+
/** Semantic body kind is authoritative when it agrees with the HTTP adapter status. */
|
|
81
7
|
static builtInError(response: HttpResponseDto): Error;
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
*
|
|
85
|
-
* {@link HttpResponseDto.body} is `unknown` because an APP owns the body shape when it installs
|
|
86
|
-
* its own translators. This default does not: a webpieces server always writes a ProtocolError
|
|
87
|
-
* here, and `ResponseBodyReader` has already parsed one. A body that is not an object at all
|
|
88
|
-
* (a bare string from something that is not a webpieces server) degrades to an EMPTY
|
|
89
|
-
* ProtocolError, so the status-to-type mapping below still answers — it never throws on the
|
|
90
|
-
* error path, which is the one path that must not fail.
|
|
91
|
-
*/
|
|
92
|
-
private static asProtocolError;
|
|
8
|
+
private static fromStatus;
|
|
9
|
+
private static fallbackMessage;
|
|
93
10
|
}
|
|
@@ -3,160 +3,66 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ClientErrorTranslator = void 0;
|
|
4
4
|
const core_util_1 = require("@webpieces/core-util");
|
|
5
5
|
const TranslatedFailure_1 = require("./TranslatedFailure");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
*
|
|
9
|
-
* This is the CLIENT-SIDE reverse of ExpressWrapper.handleError() on the server.
|
|
10
|
-
* It reconstructs typed HttpError exceptions from ProtocolError JSON responses.
|
|
11
|
-
*
|
|
12
|
-
* Architecture:
|
|
13
|
-
* - Server: HttpError → ExpressWrapper.handleError() → an {@link HttpResponseDto} on the wire
|
|
14
|
-
* - Client: that response → ClientErrorTranslator.translateError() → TranslatedFailure
|
|
15
|
-
*
|
|
16
|
-
* Both sides speak {@link HttpResponseDto}, which is what makes an app's `ErrorTranslators` one
|
|
17
|
-
* object with two halves that can be read against each other: `toWire` produces exactly the shape
|
|
18
|
-
* `fromWire` consumes, whether the reader was `http-client-node` or `http-client-browser`.
|
|
19
|
-
*
|
|
20
|
-
* This achieves symmetric error handling - server throws typed exceptions,
|
|
21
|
-
* client receives typed exceptions.
|
|
22
|
-
*
|
|
23
|
-
* The symmetry is in the TYPE and the structured fields, NOT in the prose: the server sends the real
|
|
24
|
-
* `Error.message` for `UserError` alone and a generic reason phrase for everything else. See
|
|
25
|
-
* {@link builtInError} and, on the server, `HttpErrorWireMapper`.
|
|
26
|
-
*
|
|
27
|
-
* It returns a {@link TranslatedFailure} rather than a bare `Error` because the mapping is only HALF
|
|
28
|
-
* the decision. It is ISOMORPHIC — the same mapping runs in a browser and in a server — and the two
|
|
29
|
-
* environments must NOT do the same thing with a downstream 4xx (see
|
|
30
|
-
* `ProxyClient.adaptDownstreamFailure`). The wrapper carries the one fact that hook cannot recover
|
|
31
|
-
* on its own: whether the APP claimed this status, or the built-in default did.
|
|
32
|
-
*/
|
|
6
|
+
const UnexpectedApiResponseError_1 = require("./UnexpectedApiResponseError");
|
|
7
|
+
/** Reconstructs transport-neutral API failures from an HTTP response. */
|
|
33
8
|
class ClientErrorTranslator {
|
|
34
|
-
|
|
35
|
-
* Parse an error response and decide which error the caller should see, and who decided it.
|
|
36
|
-
*
|
|
37
|
-
* The app's `ErrorTranslators` wins, so an app can reconstruct its OWN error types (e.g. a
|
|
38
|
-
* custom 460) AND override built-ins. `undefined` means "not mine" — fall through to
|
|
39
|
-
* {@link builtInError}, which stays the generic default. Symmetric with the server's
|
|
40
|
-
* ExpressWrapper.handleError(), which consults ClientRegistry.tryTranslateToWire() first.
|
|
41
|
-
*
|
|
42
|
-
* @param response - the WHOLE response, normalised out of the transport by
|
|
43
|
-
* `HttpResponseDtoFactory` — status code, reason phrase, header list and parsed body
|
|
44
|
-
* @returns the chosen error plus its provenance and the downstream status
|
|
45
|
-
*/
|
|
46
|
-
// webpieces-disable no-function-outside-class -- pure, stateless status-to-type mapping with nothing to inject, called from a BROWSER bundle where no DI container exists; static is the established idiom of this class
|
|
9
|
+
// webpieces-disable no-function-outside-class -- pure stateless mapping shared by browser and node
|
|
47
10
|
static translateError(response) {
|
|
48
|
-
const statusCode = response.status.code;
|
|
49
11
|
const custom = core_util_1.ClientRegistry.tryTranslateFromWire(response);
|
|
50
|
-
if (custom !== undefined)
|
|
51
|
-
return new TranslatedFailure_1.TranslatedFailure(custom, true,
|
|
52
|
-
|
|
53
|
-
return new TranslatedFailure_1.TranslatedFailure(ClientErrorTranslator.builtInError(response), false, statusCode);
|
|
12
|
+
if (custom !== undefined)
|
|
13
|
+
return new TranslatedFailure_1.TranslatedFailure(custom, true, response.status.code);
|
|
14
|
+
return new TranslatedFailure_1.TranslatedFailure(this.builtInError(response), false, response.status.code);
|
|
54
15
|
}
|
|
55
|
-
/**
|
|
56
|
-
|
|
57
|
-
* - 400 → BadRequestError (with field, guiAlertMessage)
|
|
58
|
-
* - 266 → UserError (with errorCode) - 2xx code for user validation
|
|
59
|
-
* - 401 → UnauthorizedError (with subType)
|
|
60
|
-
* - 403 → ForbiddenError
|
|
61
|
-
* - 404 → NotFoundError
|
|
62
|
-
* - 408 → RequestTimeoutError
|
|
63
|
-
* - 429 → TooManyRequestsError
|
|
64
|
-
* - 500 → InternalError
|
|
65
|
-
* - 502 → BadGatewayError
|
|
66
|
-
* - 503 → ServiceUnavailableError
|
|
67
|
-
* - 504 → GatewayTimeoutError
|
|
68
|
-
* - 598 → VendorError (with waitSeconds) - custom status code
|
|
69
|
-
* - other → generic HttpError
|
|
70
|
-
*
|
|
71
|
-
* # What `message` means on THIS side of the wire
|
|
72
|
-
*
|
|
73
|
-
* The reconstructed error carries whatever text the wire carried, and for every status except
|
|
74
|
-
* **266** a webpieces server deliberately sends only the GENERIC reason phrase — 'Not Found',
|
|
75
|
-
* 'Internal Server Error', … See `HttpErrorWireMapper` (http-server) for why: `Error.message` is
|
|
76
|
-
* an operator-facing field that routinely quotes internal detail, so it stays in the server's log
|
|
77
|
-
* and never reaches a caller. `UserError` (266) is the one type whose message was WRITTEN for
|
|
78
|
-
* a human to read, and it arrives verbatim.
|
|
79
|
-
*
|
|
80
|
-
* So: branch on the TYPE, on `subType`, on `errorCode`, or on `guiAlertMessage` — never on the
|
|
81
|
-
* prose of `message`. It is now a constant per status by design, and treating it as diagnostic
|
|
82
|
-
* information will not work against a current webpieces server. The diagnosis lives in the
|
|
83
|
-
* server's logs, correlated by request id.
|
|
84
|
-
*
|
|
85
|
-
* (An app that publishes richer text on purpose does it through
|
|
86
|
-
* `ClientRegistry.setErrorTranslators()`, which is consulted before this mapping on both sides.)
|
|
87
|
-
*
|
|
88
|
-
* PUBLIC, and the client-side twin of `HttpErrorWireMapper.toResponse` being public on the
|
|
89
|
-
* server: the webpieces DEFAULT is delegable, so an app whose `fromWire` claims one status can
|
|
90
|
-
* hand every other status straight back here instead of copying this ladder.
|
|
91
|
-
*/
|
|
92
|
-
// webpieces-disable no-function-outside-class -- public delegable default alongside the static above; same reason
|
|
16
|
+
/** Semantic body kind is authoritative when it agrees with the HTTP adapter status. */
|
|
17
|
+
// webpieces-disable no-function-outside-class -- public delegable default mapping
|
|
93
18
|
static builtInError(response) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
19
|
+
if (core_util_1.ApiErrorCodec.isPayload(response.body)) {
|
|
20
|
+
const decoded = core_util_1.ApiErrorCodec.decode(response.body);
|
|
21
|
+
if (core_util_1.ApiErrorHttpStatus.hasCode(decoded) &&
|
|
22
|
+
core_util_1.ApiErrorHttpStatus.code(decoded) === response.status.code)
|
|
23
|
+
return decoded;
|
|
24
|
+
return new core_util_1.ApiImplementationError('Internal Error', undefined, true);
|
|
25
|
+
}
|
|
26
|
+
return this.fromStatus(response.status.code, this.fallbackMessage(response.body, response.status.reason));
|
|
27
|
+
}
|
|
28
|
+
// webpieces-disable no-function-outside-class -- fallback for non-Webpieces HTTP responders
|
|
29
|
+
static fromStatus(statusCode, message) {
|
|
98
30
|
switch (statusCode) {
|
|
31
|
+
case 266:
|
|
32
|
+
return new core_util_1.ApiEndUserError(message);
|
|
99
33
|
case 400:
|
|
100
|
-
return new core_util_1.
|
|
101
|
-
case 266: // UserError - 2xx code for user validation errors
|
|
102
|
-
return new core_util_1.UserError(message, protocolError.errorCode);
|
|
34
|
+
return new core_util_1.ApiBadRequestError(message);
|
|
103
35
|
case 401:
|
|
104
|
-
return new core_util_1.
|
|
36
|
+
return new core_util_1.ApiUnauthorizedError(message);
|
|
105
37
|
case 403:
|
|
106
|
-
return new core_util_1.
|
|
38
|
+
return new core_util_1.ApiForbiddenError(message);
|
|
107
39
|
case 404:
|
|
108
|
-
return new core_util_1.
|
|
40
|
+
return new core_util_1.ApiNotFoundError(message);
|
|
109
41
|
case 408:
|
|
110
|
-
return new core_util_1.
|
|
42
|
+
return new core_util_1.ApiRequestTimeoutError(message);
|
|
111
43
|
case 429:
|
|
112
|
-
|
|
113
|
-
// 'Too Many Requests' for it); the client had no case for it, so it arrived as a bare
|
|
114
|
-
// HttpError and callers were pushed back to `err.code === 429` — the untyped pattern
|
|
115
|
-
// this ladder exists to replace. That gap bites harder now that `message` is a
|
|
116
|
-
// constant per status: branching on the TYPE is the only thing left, so every status
|
|
117
|
-
// the server can emit needs one.
|
|
118
|
-
return new core_util_1.TooManyRequestsError(message);
|
|
44
|
+
return new core_util_1.ApiRateLimitedError(message);
|
|
119
45
|
case 500:
|
|
120
|
-
return new core_util_1.
|
|
46
|
+
return new core_util_1.ApiImplementationError('Internal Error', undefined, true);
|
|
121
47
|
case 502:
|
|
122
|
-
return new core_util_1.
|
|
48
|
+
return new core_util_1.ApiDependencyError(message);
|
|
123
49
|
case 503:
|
|
124
|
-
return new core_util_1.
|
|
50
|
+
return new core_util_1.ApiUnavailableError(message);
|
|
125
51
|
case 504:
|
|
126
|
-
return new core_util_1.
|
|
127
|
-
case 598: // VendorError - custom status code for vendor/external service errors
|
|
128
|
-
return new core_util_1.VendorError(message, protocolError.waitSeconds);
|
|
52
|
+
return new core_util_1.ApiDependencyTimeoutError(message);
|
|
129
53
|
default:
|
|
130
|
-
|
|
131
|
-
// `err instanceof HttpError` holds after the RPC hop), carrying the status code.
|
|
132
|
-
return new core_util_1.HttpError(message || `could not translate statusCode=${statusCode}`, statusCode, subType);
|
|
54
|
+
return new UnexpectedApiResponseError_1.UnexpectedApiResponseError(statusCode, message);
|
|
133
55
|
}
|
|
134
56
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
* {@link HttpResponseDto.body} is `unknown` because an APP owns the body shape when it installs
|
|
139
|
-
* its own translators. This default does not: a webpieces server always writes a ProtocolError
|
|
140
|
-
* here, and `ResponseBodyReader` has already parsed one. A body that is not an object at all
|
|
141
|
-
* (a bare string from something that is not a webpieces server) degrades to an EMPTY
|
|
142
|
-
* ProtocolError, so the status-to-type mapping below still answers — it never throws on the
|
|
143
|
-
* error path, which is the one path that must not fail.
|
|
144
|
-
*/
|
|
145
|
-
// webpieces-disable no-any-unknown -- HttpResponseDto.body is app-owned; this narrows it back to the shape the BUILT-IN ladder reads
|
|
146
|
-
// webpieces-disable no-function-outside-class -- private helper of the statics above; same reason
|
|
147
|
-
static asProtocolError(body) {
|
|
148
|
-
const parsed = new core_util_1.ProtocolError();
|
|
57
|
+
// webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them
|
|
58
|
+
// webpieces-disable no-function-outside-class -- bounds diagnostic text from a foreign responder
|
|
59
|
+
static fallbackMessage(body, reason) {
|
|
149
60
|
if (typeof body === 'object' && body !== null) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
parsed[key] = value.slice(0, 4096);
|
|
154
|
-
}
|
|
155
|
-
const wait = Object.getOwnPropertyDescriptor(body, 'waitSeconds')?.value;
|
|
156
|
-
if (typeof wait === 'number' && Number.isFinite(wait) && wait >= 0)
|
|
157
|
-
parsed.waitSeconds = Math.min(wait, 86400);
|
|
61
|
+
const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;
|
|
62
|
+
if (typeof message === 'string' && message.length > 0)
|
|
63
|
+
return message.slice(0, 4096);
|
|
158
64
|
}
|
|
159
|
-
return
|
|
65
|
+
return reason || 'Request Failed';
|
|
160
66
|
}
|
|
161
67
|
}
|
|
162
68
|
exports.ClientErrorTranslator = ClientErrorTranslator;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ClientErrorTranslator.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/ClientErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ProtocolError,\n ClientRegistry,\n HttpResponseDto,\n HttpError,\n BadRequestError,\n UserError,\n VendorError,\n UnauthorizedError,\n ForbiddenError,\n NotFoundError,\n RequestTimeoutError,\n InternalError,\n BadGatewayError,\n ServiceUnavailableError,\n GatewayTimeoutError,\n TooManyRequestsError,\n} from '@webpieces/core-util';\nimport { TranslatedFailure } from './TranslatedFailure';\n\n/**\n * ClientErrorTranslator - Translates HTTP error responses to HttpError exceptions.\n *\n * This is the CLIENT-SIDE reverse of ExpressWrapper.handleError() on the server.\n * It reconstructs typed HttpError exceptions from ProtocolError JSON responses.\n *\n * Architecture:\n * - Server: HttpError → ExpressWrapper.handleError() → an {@link HttpResponseDto} on the wire\n * - Client: that response → ClientErrorTranslator.translateError() → TranslatedFailure\n *\n * Both sides speak {@link HttpResponseDto}, which is what makes an app's `ErrorTranslators` one\n * object with two halves that can be read against each other: `toWire` produces exactly the shape\n * `fromWire` consumes, whether the reader was `http-client-node` or `http-client-browser`.\n *\n * This achieves symmetric error handling - server throws typed exceptions,\n * client receives typed exceptions.\n *\n * The symmetry is in the TYPE and the structured fields, NOT in the prose: the server sends the real\n * `Error.message` for `UserError` alone and a generic reason phrase for everything else. See\n * {@link builtInError} and, on the server, `HttpErrorWireMapper`.\n *\n * It returns a {@link TranslatedFailure} rather than a bare `Error` because the mapping is only HALF\n * the decision. It is ISOMORPHIC — the same mapping runs in a browser and in a server — and the two\n * environments must NOT do the same thing with a downstream 4xx (see\n * `ProxyClient.adaptDownstreamFailure`). The wrapper carries the one fact that hook cannot recover\n * on its own: whether the APP claimed this status, or the built-in default did.\n */\nexport class ClientErrorTranslator {\n /**\n * Parse an error response and decide which error the caller should see, and who decided it.\n *\n * The app's `ErrorTranslators` wins, so an app can reconstruct its OWN error types (e.g. a\n * custom 460) AND override built-ins. `undefined` means \"not mine\" — fall through to\n * {@link builtInError}, which stays the generic default. Symmetric with the server's\n * ExpressWrapper.handleError(), which consults ClientRegistry.tryTranslateToWire() first.\n *\n * @param response - the WHOLE response, normalised out of the transport by\n * `HttpResponseDtoFactory` — status code, reason phrase, header list and parsed body\n * @returns the chosen error plus its provenance and the downstream status\n */\n // webpieces-disable no-function-outside-class -- pure, stateless status-to-type mapping with nothing to inject, called from a BROWSER bundle where no DI container exists; static is the established idiom of this class\n static translateError(response: HttpResponseDto): TranslatedFailure {\n const statusCode = response.status.code;\n\n const custom = ClientRegistry.tryTranslateFromWire(response);\n if (custom !== undefined) {\n return new TranslatedFailure(custom, true, statusCode);\n }\n\n return new TranslatedFailure(\n ClientErrorTranslator.builtInError(response),\n false,\n statusCode,\n );\n }\n\n /**\n * The built-in status → error mapping (symmetric with the server's ExpressWrapper.handleError()):\n * - 400 → BadRequestError (with field, guiAlertMessage)\n * - 266 → UserError (with errorCode) - 2xx code for user validation\n * - 401 → UnauthorizedError (with subType)\n * - 403 → ForbiddenError\n * - 404 → NotFoundError\n * - 408 → RequestTimeoutError\n * - 429 → TooManyRequestsError\n * - 500 → InternalError\n * - 502 → BadGatewayError\n * - 503 → ServiceUnavailableError\n * - 504 → GatewayTimeoutError\n * - 598 → VendorError (with waitSeconds) - custom status code\n * - other → generic HttpError\n *\n * # What `message` means on THIS side of the wire\n *\n * The reconstructed error carries whatever text the wire carried, and for every status except\n * **266** a webpieces server deliberately sends only the GENERIC reason phrase — 'Not Found',\n * 'Internal Server Error', … See `HttpErrorWireMapper` (http-server) for why: `Error.message` is\n * an operator-facing field that routinely quotes internal detail, so it stays in the server's log\n * and never reaches a caller. `UserError` (266) is the one type whose message was WRITTEN for\n * a human to read, and it arrives verbatim.\n *\n * So: branch on the TYPE, on `subType`, on `errorCode`, or on `guiAlertMessage` — never on the\n * prose of `message`. It is now a constant per status by design, and treating it as diagnostic\n * information will not work against a current webpieces server. The diagnosis lives in the\n * server's logs, correlated by request id.\n *\n * (An app that publishes richer text on purpose does it through\n * `ClientRegistry.setErrorTranslators()`, which is consulted before this mapping on both sides.)\n *\n * PUBLIC, and the client-side twin of `HttpErrorWireMapper.toResponse` being public on the\n * server: the webpieces DEFAULT is delegable, so an app whose `fromWire` claims one status can\n * hand every other status straight back here instead of copying this ladder.\n */\n // webpieces-disable no-function-outside-class -- public delegable default alongside the static above; same reason\n public static builtInError(response: HttpResponseDto): Error {\n const statusCode = response.status.code;\n const protocolError = ClientErrorTranslator.asProtocolError(response.body);\n const message = protocolError.message || response.status.reason || 'Unknown error';\n const subType = protocolError.subType;\n\n switch (statusCode) {\n case 400:\n return new BadRequestError(\n message,\n protocolError.field,\n protocolError.guiAlertMessage,\n );\n\n case 266: // UserError - 2xx code for user validation errors\n return new UserError(message, protocolError.errorCode);\n\n case 401:\n return new UnauthorizedError(message, subType);\n\n case 403:\n return new ForbiddenError(message);\n\n case 404:\n return new NotFoundError(message);\n\n case 408:\n return new RequestTimeoutError(message);\n\n case 429:\n // The server has always been able to throw this (HttpErrorWireMapper sends\n // 'Too Many Requests' for it); the client had no case for it, so it arrived as a bare\n // HttpError and callers were pushed back to `err.code === 429` — the untyped pattern\n // this ladder exists to replace. That gap bites harder now that `message` is a\n // constant per status: branching on the TYPE is the only thing left, so every status\n // the server can emit needs one.\n return new TooManyRequestsError(message);\n\n case 500:\n return new InternalError(message);\n\n case 502:\n return new BadGatewayError(message);\n\n case 503:\n return new ServiceUnavailableError(message);\n\n case 504:\n return new GatewayTimeoutError(message);\n\n case 598: // VendorError - custom status code for vendor/external service errors\n return new VendorError(message, protocolError.waitSeconds);\n\n default:\n // Unknown status code and no app translation claimed it: still a real HttpError (so\n // `err instanceof HttpError` holds after the RPC hop), carrying the status code.\n return new HttpError(\n message || `could not translate statusCode=${statusCode}`,\n statusCode,\n subType,\n );\n }\n }\n\n /**\n * The response body as the {@link ProtocolError} this built-in ladder reads.\n *\n * {@link HttpResponseDto.body} is `unknown` because an APP owns the body shape when it installs\n * its own translators. This default does not: a webpieces server always writes a ProtocolError\n * here, and `ResponseBodyReader` has already parsed one. A body that is not an object at all\n * (a bare string from something that is not a webpieces server) degrades to an EMPTY\n * ProtocolError, so the status-to-type mapping below still answers — it never throws on the\n * error path, which is the one path that must not fail.\n */\n // webpieces-disable no-any-unknown -- HttpResponseDto.body is app-owned; this narrows it back to the shape the BUILT-IN ladder reads\n // webpieces-disable no-function-outside-class -- private helper of the statics above; same reason\n private static asProtocolError(body: unknown): ProtocolError {\n const parsed = new ProtocolError();\n if (typeof body === 'object' && body !== null) {\n for (const key of ['message', 'subType', 'field', 'guiAlertMessage', 'errorCode'] as const) {\n const value = Object.getOwnPropertyDescriptor(body, key)?.value;\n if (typeof value === 'string') parsed[key] = value.slice(0, 4096);\n }\n const wait = Object.getOwnPropertyDescriptor(body, 'waitSeconds')?.value;\n if (typeof wait === 'number' && Number.isFinite(wait) && wait >= 0) parsed.waitSeconds = Math.min(wait, 86400);\n }\n return parsed;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ClientErrorTranslator.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/ClientErrorTranslator.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n ApiBadRequestError,\n ApiDependencyError,\n ApiDependencyTimeoutError,\n ApiEndUserError,\n ApiErrorCodec,\n ApiForbiddenError,\n ApiImplementationError,\n ApiNotFoundError,\n ApiRateLimitedError,\n ApiRequestTimeoutError,\n ApiUnauthorizedError,\n ApiUnavailableError,\n ClientRegistry,\n ApiErrorHttpStatus,\n HttpResponseDto,\n} from '@webpieces/core-util';\nimport { TranslatedFailure } from './TranslatedFailure';\nimport { UnexpectedApiResponseError } from './UnexpectedApiResponseError';\n\n/** Reconstructs transport-neutral API failures from an HTTP response. */\nexport class ClientErrorTranslator {\n // webpieces-disable no-function-outside-class -- pure stateless mapping shared by browser and node\n static translateError(response: HttpResponseDto): TranslatedFailure {\n const custom = ClientRegistry.tryTranslateFromWire(response);\n if (custom !== undefined) return new TranslatedFailure(custom, true, response.status.code);\n return new TranslatedFailure(this.builtInError(response), false, response.status.code);\n }\n\n /** Semantic body kind is authoritative when it agrees with the HTTP adapter status. */\n // webpieces-disable no-function-outside-class -- public delegable default mapping\n static builtInError(response: HttpResponseDto): Error {\n if (ApiErrorCodec.isPayload(response.body)) {\n const decoded = ApiErrorCodec.decode(response.body);\n if (\n ApiErrorHttpStatus.hasCode(decoded) &&\n ApiErrorHttpStatus.code(decoded) === response.status.code\n )\n return decoded;\n return new ApiImplementationError('Internal Error', undefined, true);\n }\n return this.fromStatus(\n response.status.code,\n this.fallbackMessage(response.body, response.status.reason),\n );\n }\n\n // webpieces-disable no-function-outside-class -- fallback for non-Webpieces HTTP responders\n private static fromStatus(statusCode: number, message: string): Error {\n switch (statusCode) {\n case 266:\n return new ApiEndUserError(message);\n case 400:\n return new ApiBadRequestError(message);\n case 401:\n return new ApiUnauthorizedError(message);\n case 403:\n return new ApiForbiddenError(message);\n case 404:\n return new ApiNotFoundError(message);\n case 408:\n return new ApiRequestTimeoutError(message);\n case 429:\n return new ApiRateLimitedError(message);\n case 500:\n return new ApiImplementationError('Internal Error', undefined, true);\n case 502:\n return new ApiDependencyError(message);\n case 503:\n return new ApiUnavailableError(message);\n case 504:\n return new ApiDependencyTimeoutError(message);\n default:\n return new UnexpectedApiResponseError(statusCode, message);\n }\n }\n\n // webpieces-disable no-any-unknown -- HTTP response bodies are app-owned until this boundary safely inspects them\n // webpieces-disable no-function-outside-class -- bounds diagnostic text from a foreign responder\n private static fallbackMessage(body: unknown, reason: string): string {\n if (typeof body === 'object' && body !== null) {\n const message = Object.getOwnPropertyDescriptor(body, 'message')?.value;\n if (typeof message === 'string' && message.length > 0) return message.slice(0, 4096);\n }\n return reason || 'Request Failed';\n }\n}\n"]}
|
package/src/ProxyClient.d.ts
CHANGED
|
@@ -189,7 +189,7 @@ export declare abstract class ProxyClient {
|
|
|
189
189
|
* edits to the url, the headers or the serialized body are exactly what goes on the wire. It may
|
|
190
190
|
* run more than once for a single RPC when a filter follows a redirect.
|
|
191
191
|
*
|
|
192
|
-
* A network reject (offline, DNS, CORS preflight) is classified into a typed
|
|
192
|
+
* A network reject (offline, DNS, CORS preflight) is classified into a typed ApiConnectionError here (a
|
|
193
193
|
* genuine bug passes through untouched) so that filters above see the same typed error the caller
|
|
194
194
|
* will, rather than a raw platform reject.
|
|
195
195
|
*/
|
package/src/ProxyClient.js
CHANGED
|
@@ -275,7 +275,7 @@ class ProxyClient {
|
|
|
275
275
|
* edits to the url, the headers or the serialized body are exactly what goes on the wire. It may
|
|
276
276
|
* run more than once for a single RPC when a filter follows a redirect.
|
|
277
277
|
*
|
|
278
|
-
* A network reject (offline, DNS, CORS preflight) is classified into a typed
|
|
278
|
+
* A network reject (offline, DNS, CORS preflight) is classified into a typed ApiConnectionError here (a
|
|
279
279
|
* genuine bug passes through untouched) so that filters above see the same typed error the caller
|
|
280
280
|
* will, rather than a raw platform reject.
|
|
281
281
|
*/
|
package/src/ProxyClient.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ProxyClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/ProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMeta,\n isFormPost,\n isRawBody,\n getMaskSpec,\n AuthMeta,\n DestinationTrust,\n RouteMetadata,\n LogApiCallImpl,\n ApiMethodInfo,\n toError,\n NetworkRejectClassifier,\n FilterChain,\n CallRegistry,\n CallDeadline,\n CallContext,\n} from '@webpieces/core-util';\nimport { ApiPrototype } from './ApiPrototype';\nimport { ClientFilterDefinition } from './ClientFilter';\nimport { ClientRequest } from './ClientRequest';\nimport { ClientErrorTranslator } from './ClientErrorTranslator';\nimport { HttpResponseDtoFactory } from './HttpResponseDtoFactory';\nimport { RequestOutcome } from './RequestOutcome';\nimport { ResponseBodyReader } from './ResponseBodyReader';\nimport { TranslatedFailure } from './TranslatedFailure';\n\n/**\n * ProxyClient - the HTTP call engine behind one API contract's client proxy.\n *\n * Contains ONLY what a browser can run: the route map built from the contract's decorators, URL\n * assembly, `fetch`, error translation, and logging. It holds no context object, no credentials,\n * and no recorder — it ASKS ITSELF for those through the hooks below, and each subclass answers\n * from its own environment.\n *\n * That is why the class is abstract rather than parameterized by a collaborator: a shared\n * header-provider seam would drag Node's AsyncLocalStorage vocabulary into a browser bundle and the\n * browser's store vocabulary into a server, and neither has any use for the other.\n *\n * NodeProxyClient (@webpieces/http-client-node) -> RequestContext, Secrets, mintIdToken, recording\n * BrowserProxyClient (@webpieces/http-client-browser) -> an app-held store, no credentials, no recording\n *\n * TWO-PHASE: collaborators arrive on the subclass constructor (so a DI container can supply them),\n * while the per-client state — which contract, which target — arrives on the subclass's `init`,\n * which calls {@link initRoutes}. That is what lets a factory hold a `Provider<ProxyClient>` and\n * hand out a fresh, independently-configured client per contract.\n */\nexport abstract class ProxyClient {\n // Assigned by initRoutes(), which every subclass's init() calls immediately after construction.\n private routeMap!: Map<string, RouteMetadata>;\n private apiName!: string;\n private apiClass!: ApiPrototype<object>;\n\n /**\n * The OUTBOUND filter chain, built once at bind time from {@link clientFilters} and reused for\n * every call. Built once rather than per call because a filter is STATELESS by contract (the\n * per-call state is the {@link ClientRequest} the chain is handed), exactly as on the server.\n */\n private chain!: FilterChain<ClientRequest, Response>;\n\n /**\n * The app's own filters, as handed to `createRpcClient`. Set by {@link initRoutes} BEFORE it\n * calls {@link clientFilters}, so an environment's built-ins may read the app's intent off them\n * — @webpieces/http-client-node takes the SSRF policy from an installed `ContextBaseUrlFilter`\n * that way, which keeps the one legitimate relaxation at the same construction site as the\n * decision to be re-pointable at all.\n */\n protected appFilters: ClientFilterDefinition[] = [];\n\n // Stateless + dependency-free, so the browser bundle keeps no DI on the fetch path.\n private readonly networkRejectClassifier = new NetworkRejectClassifier();\n\n // Same shape and same reason: stateless, so it is constructed here rather than injected.\n private readonly bodyReader = new ResponseBodyReader();\n\n /**\n * fetch `Response` -> the transport-neutral {@link HttpResponseDto} an app's `ErrorTranslators`\n * sees. Normalising HERE is what makes `fromWire` receive the identical shape in node and in the\n * browser: both environments share this class, and this is the only place either builds a DTO.\n */\n private readonly responseDtoFactory = new HttpResponseDtoFactory();\n\n /**\n * @param logApiCall - built by the SUBCLASS's package around that environment's ApiCallContext\n * (node: RequestContextApiCallContext; browser: BrowserApiCallContext). REQUIRED, with no\n * default: core-util cannot construct either one, and a default here would have to reach for a\n * process-global — which is exactly the throw-on-first-call this parameter deleted.\n */\n constructor(protected readonly logApiCall: LogApiCallImpl) {}\n\n // ---------------------------------------------------------------- environment hooks\n\n /** The callee's base URL. Async because a server may derive it from container metadata. */\n protected abstract resolveBaseUrl(): Promise<string>;\n\n /**\n * Context headers to put on the wire. Server reads RequestContext; browser reads its store.\n *\n * `destination` is derived from THIS route's auth mode and decides whether TRUSTED context keys\n * (`x-user-id`, `x-org-id`, `x-webpieces-roles`) may ride along — see {@link DestinationTrust}.\n * It is a required argument on purpose: a defaulted \"send everything\" would put the permissive\n * answer one keystroke away and make the safe one opt-in.\n *\n * RENAMED from `outboundHeaders()` in the same change that added `destination`, and the rename IS\n * the migration. TypeScript accepts an override that declares FEWER parameters than its base, so a\n * downstream `protected override outboundHeaders(): Map<string, string>` would have kept compiling\n * and silently ignored the gate — the permissive behaviour surviving as a second spelling. Against\n * the NEW name that subclass fails twice over: `override` names a member the base no longer has,\n * and this abstract member is left unimplemented.\n */\n protected abstract outboundContextHeaders(destination: DestinationTrust): Map<string, string>;\n\n /**\n * Run the call. The default just logs it. Test-case RECORDING is a server concept, so\n * NodeProxyClient overrides this to capture the call when a recorder is in the context.\n *\n * Context fields are NOT passed in: a logging backend stamps them onto every record itself.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n protected 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 // apiClass = the CONTRACT name (this.apiName, e.g. 'SaveApi') so this client log line MATCHES\n // the server's for the same call. A client has no impl class, so controllerName is omitted.\n const info = new ApiMethodInfo(\n 'client',\n this.apiName,\n route.methodName,\n undefined,\n route.mask,\n );\n return this.logApiCall.execute(info, requestDto, method);\n }\n\n /**\n * Reject, at bind time, an endpoint this environment cannot satisfy — e.g. a browser cannot\n * mint the OIDC token an @WpAuthOidc endpoint demands. Surfacing it here beats failing on the\n * first call in production. The default accepts everything.\n */\n protected assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void {}\n\n /**\n * The FRAMEWORK filters this environment installs on every client it builds, BENEATH whatever\n * the app passed to `createRpcClient`. The default installs none, so the browser runs the exact\n * code path it ran before the chain existed.\n *\n * \"Beneath\" is not a priority — see {@link initRoutes}. These are the filters that must judge\n * and sign what is ACTUALLY about to be sent, so no app priority may be allowed to get under\n * them: @webpieces/http-client-node installs its SSRF guard and its outbound-auth minter here,\n * and both would be defeated by an app filter that re-pointed the URL below them. Neither\n * concept can live in this class, because reading a RequestContext, resolving DNS and minting\n * an OIDC token are all things a browser bundle must never contain.\n */\n protected clientFilters(): ClientFilterDefinition[] {\n return [];\n }\n\n /**\n * Adapt a translated downstream failure into the error THIS environment's caller should see.\n *\n * THE INVARIANT, and the reason this hook exists at all:\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 * That invariant reads differently in the two environments, which is exactly why the ISOMORPHIC\n * {@link ClientErrorTranslator} cannot settle it:\n * - BROWSER: the client IS the end user's agent, so the downstream IS the answer. Pass it through\n * unchanged.\n * - NODE: server-to-server. A 4xx from a dependency is a caller-side defect (wrong path, wrong\n * base URL, an undeployed dependency, bad service credentials), so the caller owns it as a 500.\n *\n * ABSTRACT, not a defaulted pass-through, for the same reason\n * {@link outboundContextHeaders} takes a required `destination`: a permissive default puts the\n * wrong answer one keystroke away. A new environment subclass must SAY which of the two it is,\n * and there are exactly two subclasses in the repo, so the compile error is the migration.\n *\n * @param failure - the translated error, its provenance (app-registered vs built-in), and the\n * downstream status\n * @param callId - `ApiName.methodName`, so a rewritten message can still name the call\n */\n protected abstract adaptDownstreamFailure(failure: TranslatedFailure, callId: string): Error;\n\n /**\n * Fires before the logical call's attempts, once per RPC — the progress \"start marker\". Symmetric with\n * {@link onRequestEnd}: every start is followed by exactly one end, on every path, so a listener\n * can drive a counter (bar on / bar off) without leaking a permanently-spinning bar.\n *\n * The default is a no-op, so every existing subclass is unaffected.\n */\n protected onRequestStart(_route: RouteMetadata): void {}\n\n /**\n * Fires exactly ONCE after the call settles, on EVERY path (2xx, HTTP error, network reject) —\n * the \"stop marker\", carrying how it settled.\n *\n * Subsumes the older header-only hook: this is the ONLY place the `fetch` Response — and thus its\n * `Headers` — exists, so an app that needs to read a response header (e.g. a server-version stamp\n * for client↔server version matching) reads `outcome.headers` after settlement\n * and on both the ok and error paths. `outcome.ok`/`outcome.error` add the success-or-error\n * signal the header-only seam could not give.\n *\n * The default is a no-op, so every existing subclass is unaffected.\n */\n protected onRequestEnd(_route: RouteMetadata, _outcome: RequestOutcome): void {}\n\n // ---------------------------------------------------------------- contract binding\n\n /**\n * Bind this client to one API contract: read @ApiPath/@Endpoint/@Auth* off the prototype and\n * build the route map once. Each subclass's `init(api, config)` stores its own config, then\n * calls this.\n *\n * @param appFilters the app's OUTBOUND filters for this client, from `createRpcClient`. They are\n * merged with {@link clientFilters} and sorted by priority, highest OUTERMOST.\n * @throws Error if the prototype lacks @ApiPath, or declares an endpoint this environment\n * cannot satisfy (see {@link assertEndpointSupported}).\n */\n protected initRoutes(\n apiPrototype: ApiPrototype<object>,\n appFilters: ClientFilterDefinition[],\n ): void {\n this.appFilters = appFilters;\n this.apiClass = apiPrototype;\n if (!isApiPath(apiPrototype)) {\n const className = apiPrototype.name || 'Unknown';\n throw new Error(`Class ${className} must be decorated with @ApiPath()`);\n }\n\n const basePath = getApiPath(apiPrototype)!;\n const endpoints = getEndpoints(apiPrototype) || {};\n\n // apiName as the class name so client logs read \"SaveApi.save\", not \"undefined.save\"\n this.apiName = apiPrototype.name || 'UnknownApi';\n\n this.routeMap = new Map<string, RouteMetadata>();\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n const fullPath = basePath + endpointPath;\n // Capture the endpoint's auth mode so the client can mint delivery auth per\n // @WpAuthOidc / @WpAuthSharedSecret, exactly as the server verifies it.\n const authMeta = getAuthMeta(apiPrototype, methodName);\n this.assertEndpointSupported(authMeta, methodName);\n const formPost = isFormPost(apiPrototype, methodName);\n this.routeMap.set(\n methodName,\n new RouteMetadata(\n 'POST',\n fullPath,\n methodName,\n this.apiName,\n authMeta,\n undefined,\n formPost,\n getMaskSpec(apiPrototype, methodName),\n isRawBody(apiPrototype, methodName),\n ),\n );\n }\n\n // APP filters first (highest priority OUTERMOST, matching the server's FilterMatcher), then\n // the framework built-ins, ALWAYS innermost. Two separate sorts rather than one over the\n // union, deliberately: an app priority orders app filters against each other and nothing\n // else, so no number an app can type — however large — gets underneath the SSRF guard or the\n // credential minter. A single sorted list would make \"displace the guard\" a matter of typing\n // a bigger integer, and a security control an app can outrank by accident is not a control.\n //\n // Sorted here, once, so FilterChain itself never sorts — priority lives on the DEFINITION,\n // not on the filter.\n const byPriority = (a: ClientFilterDefinition, b: ClientFilterDefinition): number =>\n b.priority - a.priority;\n const ordered = [\n ...[...this.appFilters].sort(byPriority),\n ...[...this.clientFilters()].sort(byPriority),\n ];\n this.chain = new FilterChain<ClientRequest, Response>(\n ordered.map((definition: ClientFilterDefinition) => definition.filter),\n );\n }\n\n /** The contract's class name, for logs and recordings. */\n protected contractName(): string {\n return this.apiName;\n }\n\n /** Check if a route exists for the given method name. */\n hasRoute(methodName: string): boolean {\n return this.routeMap.has(methodName);\n }\n\n /**\n * Get route metadata for a method name.\n * @throws Error if no route found\n */\n getRoute(methodName: string): RouteMetadata {\n const route = this.routeMap.get(methodName);\n if (!route) {\n throw new Error(`No route found for method ${methodName}`);\n }\n return route;\n }\n\n // ---------------------------------------------------------------- the call\n\n /**\n * FAIL FAST, PER METHOD, at call time: some endpoints exist for a caller that is not us, and this\n * proxy could only ever build a request they are obliged to reject. Refusing here rather than at\n * bind time means an api that MIXES such endpoints with normal ones still yields a working client\n * for the normal ones; only calling the un-callable method throws.\n *\n * @throws Error naming the endpoint, what it declared, and who its real caller is.\n */\n private refuseEndpointNoClientCanCall(route: RouteMetadata): void {\n // formPost exists ONLY for EXTERNAL inbound webhooks (e.g. Twilio is the caller). This proxy\n // JSON.stringifies the body, so calling one would silently send a wrong-encoded body.\n if (route.formPost) {\n throw new Error(\n `${this.apiName}.${route.methodName} is @Endpoint(..., { formPost: true }) — the ` +\n `webpieces client does not support calling form-encoded endpoints yet. formPost is ` +\n `for EXTERNAL inbound webhooks (e.g. Twilio) only. If this endpoint needs a ` +\n `service-to-service client, set formPost:false (or remove it) so it uses JSON.`,\n );\n }\n const authMode = route.authMeta?.mode;\n // @WpAuthApiKey: the credential is a CUSTOMER-held key, and the header carrying it is the app's\n // ApiKeyHook's choice, so this client has nothing to send and the call is a guaranteed 401.\n if (authMode?.kind === 'apikey') {\n throw new Error(\n `${this.apiName}.${route.methodName} is @WpAuthApiKey('${authMode.regime}') — only the partner ` +\n `holding that api key can call it, and the header carrying it is the app's ApiKeyHook's choice, ` +\n `so a webpieces client has no credential to send.`,\n );\n }\n // @WpAuthWebhook is DELIBERATELY absent from this list. It used to be here, on the assumption\n // that the vendor is always somebody else — but `@WpAuthWebhook(name)` names a signing SCHEME,\n // not a direction, and for an OUTBOUND partner webhook WE are the vendor. The environment's\n // outbound-auth filter asks its bound signer to produce the signature, which is the exact\n // mirror of the inbound WebhookAuthCallback that verifies one.\n }\n\n /** One logical call: one lifecycle pair and log entry across all strategy attempts. */\n // webpieces-disable no-any-unknown -- request and response DTOs are erased at the proxy boundary\n async makeRequest(route: RouteMetadata, args: unknown[]): Promise<unknown> {\n this.refuseEndpointNoClientCanCall(route);\n const requestDto = args[0];\n return this.execute(route, requestDto, () => this.executeCall(route, requestDto));\n }\n\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n private async executeCall(route: RouteMetadata, requestDto: unknown): Promise<unknown> {\n this.onRequestStart(route);\n let response: Response | undefined;\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n let result: unknown;\n // webpieces-disable no-unmanaged-exceptions -- report one logical END, preserving the original thrown value\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n result = await CallRegistry.execute(\n this.apiClass,\n route.methodName,\n (timeoutMs: number) => {\n response = undefined;\n return CallDeadline.run(\n timeoutMs,\n new CallContext(this.apiName, route.methodName),\n async (signal: AbortSignal) => {\n const request = await this.prepareRequest(route, requestDto);\n CallDeadline.throwIfAborted(signal);\n const received = await this.chain.execute(request, () =>\n this.sendOnce(request, signal),\n );\n CallDeadline.throwIfAborted(signal);\n response = received;\n return this.readResponse(received, route);\n },\n );\n },\n 30_000,\n );\n } catch (err: unknown) {\n const error = toError(err);\n this.onRequestEnd(\n route,\n new RequestOutcome(false, response?.status ?? 0, response?.headers, error),\n );\n throw err;\n }\n this.onRequestEnd(\n route,\n new RequestOutcome(true, response?.status ?? 0, response?.headers),\n );\n return result;\n }\n\n /** Fresh mutable request for every attempt, including URL, headers, auth and body. */\n // webpieces-disable no-any-unknown -- request DTO is erased at the proxy boundary\n private async prepareRequest(\n route: RouteMetadata,\n requestDto: unknown,\n ): Promise<ClientRequest> {\n const baseUrl = await this.resolveBaseUrl();\n const headers = new Map<string, string>([['Content-Type', 'application/json']]);\n const context = this.outboundContextHeaders(\n DestinationTrust.forAuthMode(route.authMeta?.mode),\n );\n for (const entry of context.entries()) headers.set(entry[0], entry[1]);\n return new ClientRequest(\n route,\n this.apiName,\n baseUrl,\n headers,\n JSON.stringify(requestDto),\n requestDto,\n );\n }\n\n /**\n * ONE transmission — the bottom of the filter chain, and the only place `fetch` is called.\n *\n * Everything it sends comes off the {@link ClientRequest} as the chain left it, so a filter's\n * edits to the url, the headers or the serialized body are exactly what goes on the wire. It may\n * run more than once for a single RPC when a filter follows a redirect.\n *\n * A network reject (offline, DNS, CORS preflight) is classified into a typed OfflineError here (a\n * genuine bug passes through untouched) so that filters above see the same typed error the caller\n * will, rather than a raw platform reject.\n */\n private async sendOnce(request: ClientRequest, signal: AbortSignal): Promise<Response> {\n CallDeadline.throwIfAborted(signal);\n const options: RequestInit = {\n method: request.route.httpMethod,\n signal,\n headers: request.headersAsRecord(),\n redirect: request.followRedirects ? 'follow' : 'manual',\n };\n if (request.body !== undefined) {\n options.body = request.body;\n }\n // webpieces-disable no-unmanaged-exceptions -- classify a network reject, then rethrow it typed\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-fetch -- this IS the generated-client implementation the rule points everyone to\n return await fetch(request.url, options);\n } catch (err: unknown) {\n const error = toError(err);\n throw this.networkRejectClassifier.toNetworkError(error, request.url);\n }\n }\n\n /** Body consumption is inside the attempt deadline, including non-JSON error bodies. */\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n private async readResponse(response: Response, route: RouteMetadata): Promise<unknown> {\n const callId = `${this.apiName}.${route.methodName}`;\n // 266 is protocol success, but its body represents an expected user exception.\n if (response.ok && response.status !== 266) {\n if (!this.bodyReader.isJson(response)) {\n throw new Error(\n this.bodyReader.describeForeignBody(response, callId, await response.text()),\n );\n }\n return response.json();\n }\n const protocolError = await this.bodyReader.readErrorBody(response, callId);\n const translated = ClientErrorTranslator.translateError(\n this.responseDtoFactory.fromFetch(response, protocolError),\n );\n throw this.adaptDownstreamFailure(translated, callId);\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"ProxyClient.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/ProxyClient.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n isApiPath,\n getApiPath,\n getEndpoints,\n getAuthMeta,\n isFormPost,\n isRawBody,\n getMaskSpec,\n AuthMeta,\n DestinationTrust,\n RouteMetadata,\n LogApiCallImpl,\n ApiMethodInfo,\n toError,\n NetworkRejectClassifier,\n FilterChain,\n CallRegistry,\n CallDeadline,\n CallContext,\n} from '@webpieces/core-util';\nimport { ApiPrototype } from './ApiPrototype';\nimport { ClientFilterDefinition } from './ClientFilter';\nimport { ClientRequest } from './ClientRequest';\nimport { ClientErrorTranslator } from './ClientErrorTranslator';\nimport { HttpResponseDtoFactory } from './HttpResponseDtoFactory';\nimport { RequestOutcome } from './RequestOutcome';\nimport { ResponseBodyReader } from './ResponseBodyReader';\nimport { TranslatedFailure } from './TranslatedFailure';\n\n/**\n * ProxyClient - the HTTP call engine behind one API contract's client proxy.\n *\n * Contains ONLY what a browser can run: the route map built from the contract's decorators, URL\n * assembly, `fetch`, error translation, and logging. It holds no context object, no credentials,\n * and no recorder — it ASKS ITSELF for those through the hooks below, and each subclass answers\n * from its own environment.\n *\n * That is why the class is abstract rather than parameterized by a collaborator: a shared\n * header-provider seam would drag Node's AsyncLocalStorage vocabulary into a browser bundle and the\n * browser's store vocabulary into a server, and neither has any use for the other.\n *\n * NodeProxyClient (@webpieces/http-client-node) -> RequestContext, Secrets, mintIdToken, recording\n * BrowserProxyClient (@webpieces/http-client-browser) -> an app-held store, no credentials, no recording\n *\n * TWO-PHASE: collaborators arrive on the subclass constructor (so a DI container can supply them),\n * while the per-client state — which contract, which target — arrives on the subclass's `init`,\n * which calls {@link initRoutes}. That is what lets a factory hold a `Provider<ProxyClient>` and\n * hand out a fresh, independently-configured client per contract.\n */\nexport abstract class ProxyClient {\n // Assigned by initRoutes(), which every subclass's init() calls immediately after construction.\n private routeMap!: Map<string, RouteMetadata>;\n private apiName!: string;\n private apiClass!: ApiPrototype<object>;\n\n /**\n * The OUTBOUND filter chain, built once at bind time from {@link clientFilters} and reused for\n * every call. Built once rather than per call because a filter is STATELESS by contract (the\n * per-call state is the {@link ClientRequest} the chain is handed), exactly as on the server.\n */\n private chain!: FilterChain<ClientRequest, Response>;\n\n /**\n * The app's own filters, as handed to `createRpcClient`. Set by {@link initRoutes} BEFORE it\n * calls {@link clientFilters}, so an environment's built-ins may read the app's intent off them\n * — @webpieces/http-client-node takes the SSRF policy from an installed `ContextBaseUrlFilter`\n * that way, which keeps the one legitimate relaxation at the same construction site as the\n * decision to be re-pointable at all.\n */\n protected appFilters: ClientFilterDefinition[] = [];\n\n // Stateless + dependency-free, so the browser bundle keeps no DI on the fetch path.\n private readonly networkRejectClassifier = new NetworkRejectClassifier();\n\n // Same shape and same reason: stateless, so it is constructed here rather than injected.\n private readonly bodyReader = new ResponseBodyReader();\n\n /**\n * fetch `Response` -> the transport-neutral {@link HttpResponseDto} an app's `ErrorTranslators`\n * sees. Normalising HERE is what makes `fromWire` receive the identical shape in node and in the\n * browser: both environments share this class, and this is the only place either builds a DTO.\n */\n private readonly responseDtoFactory = new HttpResponseDtoFactory();\n\n /**\n * @param logApiCall - built by the SUBCLASS's package around that environment's ApiCallContext\n * (node: RequestContextApiCallContext; browser: BrowserApiCallContext). REQUIRED, with no\n * default: core-util cannot construct either one, and a default here would have to reach for a\n * process-global — which is exactly the throw-on-first-call this parameter deleted.\n */\n constructor(protected readonly logApiCall: LogApiCallImpl) {}\n\n // ---------------------------------------------------------------- environment hooks\n\n /** The callee's base URL. Async because a server may derive it from container metadata. */\n protected abstract resolveBaseUrl(): Promise<string>;\n\n /**\n * Context headers to put on the wire. Server reads RequestContext; browser reads its store.\n *\n * `destination` is derived from THIS route's auth mode and decides whether TRUSTED context keys\n * (`x-user-id`, `x-org-id`, `x-webpieces-roles`) may ride along — see {@link DestinationTrust}.\n * It is a required argument on purpose: a defaulted \"send everything\" would put the permissive\n * answer one keystroke away and make the safe one opt-in.\n *\n * RENAMED from `outboundHeaders()` in the same change that added `destination`, and the rename IS\n * the migration. TypeScript accepts an override that declares FEWER parameters than its base, so a\n * downstream `protected override outboundHeaders(): Map<string, string>` would have kept compiling\n * and silently ignored the gate — the permissive behaviour surviving as a second spelling. Against\n * the NEW name that subclass fails twice over: `override` names a member the base no longer has,\n * and this abstract member is left unimplemented.\n */\n protected abstract outboundContextHeaders(destination: DestinationTrust): Map<string, string>;\n\n /**\n * Run the call. The default just logs it. Test-case RECORDING is a server concept, so\n * NodeProxyClient overrides this to capture the call when a recorder is in the context.\n *\n * Context fields are NOT passed in: a logging backend stamps them onto every record itself.\n */\n // webpieces-disable no-any-unknown -- DTO types are erased at the proxy boundary\n protected 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 // apiClass = the CONTRACT name (this.apiName, e.g. 'SaveApi') so this client log line MATCHES\n // the server's for the same call. A client has no impl class, so controllerName is omitted.\n const info = new ApiMethodInfo(\n 'client',\n this.apiName,\n route.methodName,\n undefined,\n route.mask,\n );\n return this.logApiCall.execute(info, requestDto, method);\n }\n\n /**\n * Reject, at bind time, an endpoint this environment cannot satisfy — e.g. a browser cannot\n * mint the OIDC token an @WpAuthOidc endpoint demands. Surfacing it here beats failing on the\n * first call in production. The default accepts everything.\n */\n protected assertEndpointSupported(_authMeta: AuthMeta | undefined, _methodName: string): void {}\n\n /**\n * The FRAMEWORK filters this environment installs on every client it builds, BENEATH whatever\n * the app passed to `createRpcClient`. The default installs none, so the browser runs the exact\n * code path it ran before the chain existed.\n *\n * \"Beneath\" is not a priority — see {@link initRoutes}. These are the filters that must judge\n * and sign what is ACTUALLY about to be sent, so no app priority may be allowed to get under\n * them: @webpieces/http-client-node installs its SSRF guard and its outbound-auth minter here,\n * and both would be defeated by an app filter that re-pointed the URL below them. Neither\n * concept can live in this class, because reading a RequestContext, resolving DNS and minting\n * an OIDC token are all things a browser bundle must never contain.\n */\n protected clientFilters(): ClientFilterDefinition[] {\n return [];\n }\n\n /**\n * Adapt a translated downstream failure into the error THIS environment's caller should see.\n *\n * THE INVARIANT, and the reason this hook exists at all:\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 * That invariant reads differently in the two environments, which is exactly why the ISOMORPHIC\n * {@link ClientErrorTranslator} cannot settle it:\n * - BROWSER: the client IS the end user's agent, so the downstream IS the answer. Pass it through\n * unchanged.\n * - NODE: server-to-server. A 4xx from a dependency is a caller-side defect (wrong path, wrong\n * base URL, an undeployed dependency, bad service credentials), so the caller owns it as a 500.\n *\n * ABSTRACT, not a defaulted pass-through, for the same reason\n * {@link outboundContextHeaders} takes a required `destination`: a permissive default puts the\n * wrong answer one keystroke away. A new environment subclass must SAY which of the two it is,\n * and there are exactly two subclasses in the repo, so the compile error is the migration.\n *\n * @param failure - the translated error, its provenance (app-registered vs built-in), and the\n * downstream status\n * @param callId - `ApiName.methodName`, so a rewritten message can still name the call\n */\n protected abstract adaptDownstreamFailure(failure: TranslatedFailure, callId: string): Error;\n\n /**\n * Fires before the logical call's attempts, once per RPC — the progress \"start marker\". Symmetric with\n * {@link onRequestEnd}: every start is followed by exactly one end, on every path, so a listener\n * can drive a counter (bar on / bar off) without leaking a permanently-spinning bar.\n *\n * The default is a no-op, so every existing subclass is unaffected.\n */\n protected onRequestStart(_route: RouteMetadata): void {}\n\n /**\n * Fires exactly ONCE after the call settles, on EVERY path (2xx, HTTP error, network reject) —\n * the \"stop marker\", carrying how it settled.\n *\n * Subsumes the older header-only hook: this is the ONLY place the `fetch` Response — and thus its\n * `Headers` — exists, so an app that needs to read a response header (e.g. a server-version stamp\n * for client↔server version matching) reads `outcome.headers` after settlement\n * and on both the ok and error paths. `outcome.ok`/`outcome.error` add the success-or-error\n * signal the header-only seam could not give.\n *\n * The default is a no-op, so every existing subclass is unaffected.\n */\n protected onRequestEnd(_route: RouteMetadata, _outcome: RequestOutcome): void {}\n\n // ---------------------------------------------------------------- contract binding\n\n /**\n * Bind this client to one API contract: read @ApiPath/@Endpoint/@Auth* off the prototype and\n * build the route map once. Each subclass's `init(api, config)` stores its own config, then\n * calls this.\n *\n * @param appFilters the app's OUTBOUND filters for this client, from `createRpcClient`. They are\n * merged with {@link clientFilters} and sorted by priority, highest OUTERMOST.\n * @throws Error if the prototype lacks @ApiPath, or declares an endpoint this environment\n * cannot satisfy (see {@link assertEndpointSupported}).\n */\n protected initRoutes(\n apiPrototype: ApiPrototype<object>,\n appFilters: ClientFilterDefinition[],\n ): void {\n this.appFilters = appFilters;\n this.apiClass = apiPrototype;\n if (!isApiPath(apiPrototype)) {\n const className = apiPrototype.name || 'Unknown';\n throw new Error(`Class ${className} must be decorated with @ApiPath()`);\n }\n\n const basePath = getApiPath(apiPrototype)!;\n const endpoints = getEndpoints(apiPrototype) || {};\n\n // apiName as the class name so client logs read \"SaveApi.save\", not \"undefined.save\"\n this.apiName = apiPrototype.name || 'UnknownApi';\n\n this.routeMap = new Map<string, RouteMetadata>();\n for (const [methodName, endpointPath] of Object.entries(endpoints)) {\n const fullPath = basePath + endpointPath;\n // Capture the endpoint's auth mode so the client can mint delivery auth per\n // @WpAuthOidc / @WpAuthSharedSecret, exactly as the server verifies it.\n const authMeta = getAuthMeta(apiPrototype, methodName);\n this.assertEndpointSupported(authMeta, methodName);\n const formPost = isFormPost(apiPrototype, methodName);\n this.routeMap.set(\n methodName,\n new RouteMetadata(\n 'POST',\n fullPath,\n methodName,\n this.apiName,\n authMeta,\n undefined,\n formPost,\n getMaskSpec(apiPrototype, methodName),\n isRawBody(apiPrototype, methodName),\n ),\n );\n }\n\n // APP filters first (highest priority OUTERMOST, matching the server's FilterMatcher), then\n // the framework built-ins, ALWAYS innermost. Two separate sorts rather than one over the\n // union, deliberately: an app priority orders app filters against each other and nothing\n // else, so no number an app can type — however large — gets underneath the SSRF guard or the\n // credential minter. A single sorted list would make \"displace the guard\" a matter of typing\n // a bigger integer, and a security control an app can outrank by accident is not a control.\n //\n // Sorted here, once, so FilterChain itself never sorts — priority lives on the DEFINITION,\n // not on the filter.\n const byPriority = (a: ClientFilterDefinition, b: ClientFilterDefinition): number =>\n b.priority - a.priority;\n const ordered = [\n ...[...this.appFilters].sort(byPriority),\n ...[...this.clientFilters()].sort(byPriority),\n ];\n this.chain = new FilterChain<ClientRequest, Response>(\n ordered.map((definition: ClientFilterDefinition) => definition.filter),\n );\n }\n\n /** The contract's class name, for logs and recordings. */\n protected contractName(): string {\n return this.apiName;\n }\n\n /** Check if a route exists for the given method name. */\n hasRoute(methodName: string): boolean {\n return this.routeMap.has(methodName);\n }\n\n /**\n * Get route metadata for a method name.\n * @throws Error if no route found\n */\n getRoute(methodName: string): RouteMetadata {\n const route = this.routeMap.get(methodName);\n if (!route) {\n throw new Error(`No route found for method ${methodName}`);\n }\n return route;\n }\n\n // ---------------------------------------------------------------- the call\n\n /**\n * FAIL FAST, PER METHOD, at call time: some endpoints exist for a caller that is not us, and this\n * proxy could only ever build a request they are obliged to reject. Refusing here rather than at\n * bind time means an api that MIXES such endpoints with normal ones still yields a working client\n * for the normal ones; only calling the un-callable method throws.\n *\n * @throws Error naming the endpoint, what it declared, and who its real caller is.\n */\n private refuseEndpointNoClientCanCall(route: RouteMetadata): void {\n // formPost exists ONLY for EXTERNAL inbound webhooks (e.g. Twilio is the caller). This proxy\n // JSON.stringifies the body, so calling one would silently send a wrong-encoded body.\n if (route.formPost) {\n throw new Error(\n `${this.apiName}.${route.methodName} is @Endpoint(..., { formPost: true }) — the ` +\n `webpieces client does not support calling form-encoded endpoints yet. formPost is ` +\n `for EXTERNAL inbound webhooks (e.g. Twilio) only. If this endpoint needs a ` +\n `service-to-service client, set formPost:false (or remove it) so it uses JSON.`,\n );\n }\n const authMode = route.authMeta?.mode;\n // @WpAuthApiKey: the credential is a CUSTOMER-held key, and the header carrying it is the app's\n // ApiKeyHook's choice, so this client has nothing to send and the call is a guaranteed 401.\n if (authMode?.kind === 'apikey') {\n throw new Error(\n `${this.apiName}.${route.methodName} is @WpAuthApiKey('${authMode.regime}') — only the partner ` +\n `holding that api key can call it, and the header carrying it is the app's ApiKeyHook's choice, ` +\n `so a webpieces client has no credential to send.`,\n );\n }\n // @WpAuthWebhook is DELIBERATELY absent from this list. It used to be here, on the assumption\n // that the vendor is always somebody else — but `@WpAuthWebhook(name)` names a signing SCHEME,\n // not a direction, and for an OUTBOUND partner webhook WE are the vendor. The environment's\n // outbound-auth filter asks its bound signer to produce the signature, which is the exact\n // mirror of the inbound WebhookAuthCallback that verifies one.\n }\n\n /** One logical call: one lifecycle pair and log entry across all strategy attempts. */\n // webpieces-disable no-any-unknown -- request and response DTOs are erased at the proxy boundary\n async makeRequest(route: RouteMetadata, args: unknown[]): Promise<unknown> {\n this.refuseEndpointNoClientCanCall(route);\n const requestDto = args[0];\n return this.execute(route, requestDto, () => this.executeCall(route, requestDto));\n }\n\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n private async executeCall(route: RouteMetadata, requestDto: unknown): Promise<unknown> {\n this.onRequestStart(route);\n let response: Response | undefined;\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n let result: unknown;\n // webpieces-disable no-unmanaged-exceptions -- report one logical END, preserving the original thrown value\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n result = await CallRegistry.execute(\n this.apiClass,\n route.methodName,\n (timeoutMs: number) => {\n response = undefined;\n return CallDeadline.run(\n timeoutMs,\n new CallContext(this.apiName, route.methodName),\n async (signal: AbortSignal) => {\n const request = await this.prepareRequest(route, requestDto);\n CallDeadline.throwIfAborted(signal);\n const received = await this.chain.execute(request, () =>\n this.sendOnce(request, signal),\n );\n CallDeadline.throwIfAborted(signal);\n response = received;\n return this.readResponse(received, route);\n },\n );\n },\n 30_000,\n );\n } catch (err: unknown) {\n const error = toError(err);\n this.onRequestEnd(\n route,\n new RequestOutcome(false, response?.status ?? 0, response?.headers, error),\n );\n throw err;\n }\n this.onRequestEnd(\n route,\n new RequestOutcome(true, response?.status ?? 0, response?.headers),\n );\n return result;\n }\n\n /** Fresh mutable request for every attempt, including URL, headers, auth and body. */\n // webpieces-disable no-any-unknown -- request DTO is erased at the proxy boundary\n private async prepareRequest(\n route: RouteMetadata,\n requestDto: unknown,\n ): Promise<ClientRequest> {\n const baseUrl = await this.resolveBaseUrl();\n const headers = new Map<string, string>([['Content-Type', 'application/json']]);\n const context = this.outboundContextHeaders(\n DestinationTrust.forAuthMode(route.authMeta?.mode),\n );\n for (const entry of context.entries()) headers.set(entry[0], entry[1]);\n return new ClientRequest(\n route,\n this.apiName,\n baseUrl,\n headers,\n JSON.stringify(requestDto),\n requestDto,\n );\n }\n\n /**\n * ONE transmission — the bottom of the filter chain, and the only place `fetch` is called.\n *\n * Everything it sends comes off the {@link ClientRequest} as the chain left it, so a filter's\n * edits to the url, the headers or the serialized body are exactly what goes on the wire. It may\n * run more than once for a single RPC when a filter follows a redirect.\n *\n * A network reject (offline, DNS, CORS preflight) is classified into a typed ApiConnectionError here (a\n * genuine bug passes through untouched) so that filters above see the same typed error the caller\n * will, rather than a raw platform reject.\n */\n private async sendOnce(request: ClientRequest, signal: AbortSignal): Promise<Response> {\n CallDeadline.throwIfAborted(signal);\n const options: RequestInit = {\n method: request.route.httpMethod,\n signal,\n headers: request.headersAsRecord(),\n redirect: request.followRedirects ? 'follow' : 'manual',\n };\n if (request.body !== undefined) {\n options.body = request.body;\n }\n // webpieces-disable no-unmanaged-exceptions -- classify a network reject, then rethrow it typed\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-fetch -- this IS the generated-client implementation the rule points everyone to\n return await fetch(request.url, options);\n } catch (err: unknown) {\n const error = toError(err);\n throw this.networkRejectClassifier.toNetworkError(error, request.url);\n }\n }\n\n /** Body consumption is inside the attempt deadline, including non-JSON error bodies. */\n // webpieces-disable no-any-unknown -- response DTO is erased at the proxy boundary\n private async readResponse(response: Response, route: RouteMetadata): Promise<unknown> {\n const callId = `${this.apiName}.${route.methodName}`;\n // 266 is protocol success, but its body represents an expected user exception.\n if (response.ok && response.status !== 266) {\n if (!this.bodyReader.isJson(response)) {\n throw new Error(\n this.bodyReader.describeForeignBody(response, callId, await response.text()),\n );\n }\n return response.json();\n }\n const protocolError = await this.bodyReader.readErrorBody(response, callId);\n const translated = ClientErrorTranslator.translateError(\n this.responseDtoFactory.fromFetch(response, protocolError),\n );\n throw this.adaptDownstreamFailure(translated, callId);\n }\n}\n"]}
|
package/src/RequestOutcome.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* The three shapes, one per path:
|
|
9
9
|
* - 2xx `new RequestOutcome(true, status, headers)` — no error
|
|
10
|
-
* - HTTP error `new RequestOutcome(false, status, headers, error)` — the translated
|
|
10
|
+
* - HTTP error `new RequestOutcome(false, status, headers, error)` — the translated API error
|
|
11
11
|
* - network reject `new RequestOutcome(false, 0, undefined, error)` — no Response ever existed
|
|
12
12
|
*
|
|
13
13
|
* A timeout or body parse failure uses the failure shape, with headers/status if they arrived.
|
package/src/RequestOutcome.js
CHANGED
|
@@ -10,7 +10,7 @@ exports.RequestOutcome = void 0;
|
|
|
10
10
|
*
|
|
11
11
|
* The three shapes, one per path:
|
|
12
12
|
* - 2xx `new RequestOutcome(true, status, headers)` — no error
|
|
13
|
-
* - HTTP error `new RequestOutcome(false, status, headers, error)` — the translated
|
|
13
|
+
* - HTTP error `new RequestOutcome(false, status, headers, error)` — the translated API error
|
|
14
14
|
* - network reject `new RequestOutcome(false, 0, undefined, error)` — no Response ever existed
|
|
15
15
|
*
|
|
16
16
|
* A timeout or body parse failure uses the failure shape, with headers/status if they arrived.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RequestOutcome.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/RequestOutcome.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;GAaG;AACH,MAAa,cAAc;IAGH;IAEA;IAMA;IAUA;IApBpB;IACI,2FAA2F;IAC3E,EAAW;IAC3B,qEAAqE;IACrD,MAAc;IAC9B;;;;OAIG;IACa,OAAiB;IACjC;;;;;;;;OAQG;IACa,KAAa;QAlBb,OAAE,GAAF,EAAE,CAAS;QAEX,WAAM,GAAN,MAAM,CAAQ;QAMd,YAAO,GAAP,OAAO,CAAU;QAUjB,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AAvBD,wCAuBC","sourcesContent":["/**\n * How one RPC call SETTLED — the payload of {@link ProxyClient.onRequestEnd}.\n *\n * DATA ONLY (no behavior), so it is a class with an explicit constructor rather than an interface:\n * `ProxyClient.executeCall` constructs it by name on settlement, and a reader\n * can see at the call site which path produced which shape.\n *\n * The three shapes, one per path:\n * - 2xx `new RequestOutcome(true, status, headers)` — no error\n * - HTTP error `new RequestOutcome(false, status, headers, error)` — the translated
|
|
1
|
+
{"version":3,"file":"RequestOutcome.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/RequestOutcome.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;GAaG;AACH,MAAa,cAAc;IAGH;IAEA;IAMA;IAUA;IApBpB;IACI,2FAA2F;IAC3E,EAAW;IAC3B,qEAAqE;IACrD,MAAc;IAC9B;;;;OAIG;IACa,OAAiB;IACjC;;;;;;;;OAQG;IACa,KAAa;QAlBb,OAAE,GAAF,EAAE,CAAS;QAEX,WAAM,GAAN,MAAM,CAAQ;QAMd,YAAO,GAAP,OAAO,CAAU;QAUjB,UAAK,GAAL,KAAK,CAAQ;IAC9B,CAAC;CACP;AAvBD,wCAuBC","sourcesContent":["/**\n * How one RPC call SETTLED — the payload of {@link ProxyClient.onRequestEnd}.\n *\n * DATA ONLY (no behavior), so it is a class with an explicit constructor rather than an interface:\n * `ProxyClient.executeCall` constructs it by name on settlement, and a reader\n * can see at the call site which path produced which shape.\n *\n * The three shapes, one per path:\n * - 2xx `new RequestOutcome(true, status, headers)` — no error\n * - HTTP error `new RequestOutcome(false, status, headers, error)` — the translated API error\n * - network reject `new RequestOutcome(false, 0, undefined, error)` — no Response ever existed\n *\n * A timeout or body parse failure uses the failure shape, with headers/status if they arrived.\n */\nexport class RequestOutcome {\n constructor(\n /** True when the logical call succeeded, including a strategy recovering from an error. */\n public readonly ok: boolean,\n /** The last attempt's HTTP status, or 0 when no response arrived. */\n public readonly status: number,\n /**\n * The Response headers, present whenever an HTTP Response existed (ok OR error) and absent\n * when no response arrived. Available after settlement, which lets an app pull\n * a server-version stamp off an error response.\n */\n public readonly headers?: Headers,\n /**\n * Set on every non-success path: for a non-2xx, the error the CALLER will see — i.e. what\n * `ClientErrorTranslator` picked AFTER `ProxyClient.adaptDownstreamFailure` had its say, so a\n * listener never disagrees with the thrown exception (on a server that is the 500 wrapping a\n * downstream 4xx, with the original reachable as `cause`). Otherwise the network/parse\n * failure normalized through `toError`. Always a real `Error` — never `unknown`, because\n * nothing here is untyped: `translateError` RETURNS a typed `TranslatedFailure`, and every\n * rejection reaching this class has been through `toError`.\n */\n public readonly error?: Error,\n ) {}\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ApiErrorPayload } from '@webpieces/core-util';
|
|
2
2
|
/**
|
|
3
3
|
* ResponseBodyReader — decides whether a response body may be `JSON.parse`d AT ALL, from its
|
|
4
4
|
* `content-type`, before anything tries.
|
|
@@ -10,9 +10,9 @@ import { ProtocolError } from '@webpieces/core-util';
|
|
|
10
10
|
* "Client Bug" dialog for a server that was merely booting.
|
|
11
11
|
*
|
|
12
12
|
* The rule, and the whole of it: **only parse a body that CLAIMS to be JSON.** Then
|
|
13
|
-
* - a non-JSON error body becomes a synthesized {@link
|
|
14
|
-
* maps by STATUS — 502 → `
|
|
15
|
-
* `
|
|
13
|
+
* - a non-JSON error body becomes a synthesized {@link ApiErrorPayload} that `ClientErrorTranslator`
|
|
14
|
+
* maps by STATUS — 502 → `ApiDependencyError`, 503 → `ApiUnavailableError`, 504 →
|
|
15
|
+
* `ApiDependencyTimeoutError` — so a caller can decide "the server is waking, retry";
|
|
16
16
|
* - a `SyntaxError` from `JSON.parse` goes back to meaning what it should: a response that SAID it
|
|
17
17
|
* was JSON and was malformed. That is a real bug, and it is exactly the signal the old
|
|
18
18
|
* parse-everything path destroyed.
|
|
@@ -21,17 +21,17 @@ export declare class ResponseBodyReader {
|
|
|
21
21
|
/** True when the response DECLARES a JSON body, and is therefore safe to `JSON.parse`. */
|
|
22
22
|
isJson(response: Response): boolean;
|
|
23
23
|
/**
|
|
24
|
-
* Read a NON-2xx body as a {@link
|
|
24
|
+
* Read a NON-2xx body as a {@link ApiErrorPayload}, whatever it turns out to be.
|
|
25
25
|
*
|
|
26
26
|
* - Declared JSON → parse it. A malformed one still throws `SyntaxError`, on purpose: the server
|
|
27
27
|
* promised JSON and broke the promise, which is a genuine defect worth surfacing as one.
|
|
28
|
-
* - Anything else → synthesize a
|
|
28
|
+
* - Anything else → synthesize a ApiErrorPayload describing what actually arrived, so the caller
|
|
29
29
|
* gets the STATUS-derived typed error instead of a parse failure.
|
|
30
30
|
*
|
|
31
31
|
* @param response - the fetch Response, already known to be non-ok
|
|
32
32
|
* @param callId - `ApiName.methodName`, so the message names the call that failed
|
|
33
33
|
*/
|
|
34
|
-
readErrorBody(response: Response, callId: string): Promise<
|
|
34
|
+
readErrorBody(response: Response, callId: string): Promise<ApiErrorPayload>;
|
|
35
35
|
/**
|
|
36
36
|
* The message for a body that is not ours. It names the status, the content-type that gave it
|
|
37
37
|
* away, and a short quote of the body — the three facts needed to tell "Google Frontend served
|
|
@@ -21,9 +21,9 @@ const SNIPPET_CHARS = 200;
|
|
|
21
21
|
* "Client Bug" dialog for a server that was merely booting.
|
|
22
22
|
*
|
|
23
23
|
* The rule, and the whole of it: **only parse a body that CLAIMS to be JSON.** Then
|
|
24
|
-
* - a non-JSON error body becomes a synthesized {@link
|
|
25
|
-
* maps by STATUS — 502 → `
|
|
26
|
-
* `
|
|
24
|
+
* - a non-JSON error body becomes a synthesized {@link ApiErrorPayload} that `ClientErrorTranslator`
|
|
25
|
+
* maps by STATUS — 502 → `ApiDependencyError`, 503 → `ApiUnavailableError`, 504 →
|
|
26
|
+
* `ApiDependencyTimeoutError` — so a caller can decide "the server is waking, retry";
|
|
27
27
|
* - a `SyntaxError` from `JSON.parse` goes back to meaning what it should: a response that SAID it
|
|
28
28
|
* was JSON and was malformed. That is a real bug, and it is exactly the signal the old
|
|
29
29
|
* parse-everything path destroyed.
|
|
@@ -38,11 +38,11 @@ class ResponseBodyReader {
|
|
|
38
38
|
return JSON_CONTENT_TYPE.test(contentType.trim());
|
|
39
39
|
}
|
|
40
40
|
/**
|
|
41
|
-
* Read a NON-2xx body as a {@link
|
|
41
|
+
* Read a NON-2xx body as a {@link ApiErrorPayload}, whatever it turns out to be.
|
|
42
42
|
*
|
|
43
43
|
* - Declared JSON → parse it. A malformed one still throws `SyntaxError`, on purpose: the server
|
|
44
44
|
* promised JSON and broke the promise, which is a genuine defect worth surfacing as one.
|
|
45
|
-
* - Anything else → synthesize a
|
|
45
|
+
* - Anything else → synthesize a ApiErrorPayload describing what actually arrived, so the caller
|
|
46
46
|
* gets the STATUS-derived typed error instead of a parse failure.
|
|
47
47
|
*
|
|
48
48
|
* @param response - the fetch Response, already known to be non-ok
|
|
@@ -52,7 +52,8 @@ class ResponseBodyReader {
|
|
|
52
52
|
if (this.isJson(response)) {
|
|
53
53
|
return (await response.json());
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
// Empty kind deliberately marks this as a foreign HTTP response so status fallback owns it.
|
|
56
|
+
const protocolError = new core_util_1.ApiErrorPayload('', 'Request Failed');
|
|
56
57
|
protocolError.message = this.describeForeignBody(response, callId, await response.text());
|
|
57
58
|
return protocolError;
|
|
58
59
|
}
|
|
@@ -64,7 +65,7 @@ class ResponseBodyReader {
|
|
|
64
65
|
describeForeignBody(response, callId, body) {
|
|
65
66
|
const contentType = response.headers.get('content-type') || '(none)';
|
|
66
67
|
return (`${callId}: HTTP ${response.status} with content-type "${contentType}" — this response did ` +
|
|
67
|
-
`not come from the webpieces server (no
|
|
68
|
+
`not come from the webpieces server (no ApiErrorPayload body). It is almost certainly ` +
|
|
68
69
|
`infrastructure: a load balancer, a proxy, or a cold start on a scale-to-zero backend. ` +
|
|
69
70
|
`body=${JSON.stringify(this.snippet(body))}`);
|
|
70
71
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResponseBodyReader.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/ResponseBodyReader.ts"],"names":[],"mappings":";;;AAAA,
|
|
1
|
+
{"version":3,"file":"ResponseBodyReader.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/ResponseBodyReader.ts"],"names":[],"mappings":";;;AAAA,oDAAuD;AAEvD;;;;GAIG;AACH,MAAM,iBAAiB,GAAG,8CAA8C,CAAC;AAEzE,qGAAqG;AACrG,MAAM,aAAa,GAAG,GAAG,CAAC;AAE1B;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAa,kBAAkB;IAC3B,0FAA0F;IAC1F,MAAM,CAAC,QAAkB;QACrB,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QACzD,IAAI,CAAC,WAAW,EAAE,CAAC;YACf,OAAO,KAAK,CAAC;QACjB,CAAC;QACD,OAAO,iBAAiB,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IAED;;;;;;;;;;OAUG;IACH,KAAK,CAAC,aAAa,CAAC,QAAkB,EAAE,MAAc;QAClD,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;YACxB,OAAO,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAoB,CAAC;QACtD,CAAC;QAED,4FAA4F;QAC5F,MAAM,aAAa,GAAG,IAAI,2BAAe,CAAC,EAAE,EAAE,gBAAgB,CAAC,CAAC;QAChE,aAAa,CAAC,OAAO,GAAG,IAAI,CAAC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;QAC1F,OAAO,aAAa,CAAC;IACzB,CAAC;IAED;;;;OAIG;IACH,mBAAmB,CAAC,QAAkB,EAAE,MAAc,EAAE,IAAY;QAChE,MAAM,WAAW,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,QAAQ,CAAC;QACrE,OAAO,CACH,GAAG,MAAM,UAAU,QAAQ,CAAC,MAAM,uBAAuB,WAAW,wBAAwB;YAC5F,uFAAuF;YACvF,wFAAwF;YACxF,QAAQ,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,EAAE,CAC/C,CAAC;IACN,CAAC;IAED,mGAAmG;IAC3F,OAAO,CAAC,IAAY;QACxB,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;QACnD,IAAI,SAAS,CAAC,MAAM,IAAI,aAAa,EAAE,CAAC;YACpC,OAAO,SAAS,CAAC;QACrB,CAAC;QACD,OAAO,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,aAAa,CAAC,GAAG,CAAC;IACnD,CAAC;CACJ;AAvDD,gDAuDC","sourcesContent":["import { ApiErrorPayload } from '@webpieces/core-util';\n\n/**\n * `application/json`, plus every `+json` structured suffix (`application/problem+json`,\n * `application/vnd.acme.v2+json`). Parameters (`; charset=utf-8`) are ignored, and matching is\n * case-insensitive because a content-type is case-insensitive on the wire.\n */\nconst JSON_CONTENT_TYPE = /^application\\/(?:[\\w.+-]+\\+)?json\\s*(?:;|$)/i;\n\n/** How much of a non-JSON body to quote in the error message — enough to identify it, not a dump. */\nconst SNIPPET_CHARS = 200;\n\n/**\n * ResponseBodyReader — decides whether a response body may be `JSON.parse`d AT ALL, from its\n * `content-type`, before anything tries.\n *\n * WHY THIS EXISTS: the client used to call `response.json()` on every body regardless of what the\n * response said it was. A scale-to-zero backend answers a cold start with the load balancer's own\n * **HTML** error page, so a 502 arrived at the app as `SyntaxError: Unexpected token '<'` — the\n * status thrown away, and infrastructure indistinguishable from a code defect. Apps showed users a\n * \"Client Bug\" dialog for a server that was merely booting.\n *\n * The rule, and the whole of it: **only parse a body that CLAIMS to be JSON.** Then\n * - a non-JSON error body becomes a synthesized {@link ApiErrorPayload} that `ClientErrorTranslator`\n * maps by STATUS — 502 → `ApiDependencyError`, 503 → `ApiUnavailableError`, 504 →\n * `ApiDependencyTimeoutError` — so a caller can decide \"the server is waking, retry\";\n * - a `SyntaxError` from `JSON.parse` goes back to meaning what it should: a response that SAID it\n * was JSON and was malformed. That is a real bug, and it is exactly the signal the old\n * parse-everything path destroyed.\n */\nexport class ResponseBodyReader {\n /** True when the response DECLARES a JSON body, and is therefore safe to `JSON.parse`. */\n isJson(response: Response): boolean {\n const contentType = response.headers.get('content-type');\n if (!contentType) {\n return false;\n }\n return JSON_CONTENT_TYPE.test(contentType.trim());\n }\n\n /**\n * Read a NON-2xx body as a {@link ApiErrorPayload}, whatever it turns out to be.\n *\n * - Declared JSON → parse it. A malformed one still throws `SyntaxError`, on purpose: the server\n * promised JSON and broke the promise, which is a genuine defect worth surfacing as one.\n * - Anything else → synthesize a ApiErrorPayload describing what actually arrived, so the caller\n * gets the STATUS-derived typed error instead of a parse failure.\n *\n * @param response - the fetch Response, already known to be non-ok\n * @param callId - `ApiName.methodName`, so the message names the call that failed\n */\n async readErrorBody(response: Response, callId: string): Promise<ApiErrorPayload> {\n if (this.isJson(response)) {\n return (await response.json()) as ApiErrorPayload;\n }\n\n // Empty kind deliberately marks this as a foreign HTTP response so status fallback owns it.\n const protocolError = new ApiErrorPayload('', 'Request Failed');\n protocolError.message = this.describeForeignBody(response, callId, await response.text());\n return protocolError;\n }\n\n /**\n * The message for a body that is not ours. It names the status, the content-type that gave it\n * away, and a short quote of the body — the three facts needed to tell \"Google Frontend served\n * its own 502 page\" apart from \"our server returned an error\".\n */\n describeForeignBody(response: Response, callId: string, body: string): string {\n const contentType = response.headers.get('content-type') || '(none)';\n return (\n `${callId}: HTTP ${response.status} with content-type \"${contentType}\" — this response did ` +\n `not come from the webpieces server (no ApiErrorPayload body). It is almost certainly ` +\n `infrastructure: a load balancer, a proxy, or a cold start on a scale-to-zero backend. ` +\n `body=${JSON.stringify(this.snippet(body))}`\n );\n }\n\n /** First {@link SNIPPET_CHARS} characters, whitespace collapsed so an HTML page stays one line. */\n private snippet(body: string): string {\n const collapsed = body.replace(/\\s+/g, ' ').trim();\n if (collapsed.length <= SNIPPET_CHARS) {\n return collapsed;\n }\n return `${collapsed.slice(0, SNIPPET_CHARS)}…`;\n }\n}\n"]}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* exactly like {@link RequestOutcome}.
|
|
7
7
|
*
|
|
8
8
|
* WHY IT CARRIES `appRegistered` AT ALL: the translated error alone is not enough for an environment
|
|
9
|
-
* hook to act on. `
|
|
9
|
+
* hook to act on. `ApiNotFoundError` produced by the BUILT-IN 404 branch and `ApiNotFoundError`
|
|
10
10
|
* produced by an app's own `ErrorTranslators` are indistinguishable as values, yet they mean opposite
|
|
11
11
|
* things — the first is the framework's generic default, the second is the app saying out loud, at
|
|
12
12
|
* startup and greppably, "relay this status as my own". `ProxyClient.adaptDownstreamFailure` must
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
*
|
|
16
16
|
* `statusCode` is the status the DOWNSTREAM answered — carried explicitly rather than read back off
|
|
17
17
|
* `error.code`, because an app-registered translation may legitimately return an error whose `code`
|
|
18
|
-
* is nothing like the status that produced it, and need not be
|
|
18
|
+
* is nothing like the status that produced it, and need not be a portable `ApiError` at all.
|
|
19
19
|
*/
|
|
20
20
|
export declare class TranslatedFailure {
|
|
21
21
|
/** The typed error the translator picked for this response. Always a real `Error`. */
|
package/src/TranslatedFailure.js
CHANGED
|
@@ -9,7 +9,7 @@ exports.TranslatedFailure = void 0;
|
|
|
9
9
|
* exactly like {@link RequestOutcome}.
|
|
10
10
|
*
|
|
11
11
|
* WHY IT CARRIES `appRegistered` AT ALL: the translated error alone is not enough for an environment
|
|
12
|
-
* hook to act on. `
|
|
12
|
+
* hook to act on. `ApiNotFoundError` produced by the BUILT-IN 404 branch and `ApiNotFoundError`
|
|
13
13
|
* produced by an app's own `ErrorTranslators` are indistinguishable as values, yet they mean opposite
|
|
14
14
|
* things — the first is the framework's generic default, the second is the app saying out loud, at
|
|
15
15
|
* startup and greppably, "relay this status as my own". `ProxyClient.adaptDownstreamFailure` must
|
|
@@ -18,7 +18,7 @@ exports.TranslatedFailure = void 0;
|
|
|
18
18
|
*
|
|
19
19
|
* `statusCode` is the status the DOWNSTREAM answered — carried explicitly rather than read back off
|
|
20
20
|
* `error.code`, because an app-registered translation may legitimately return an error whose `code`
|
|
21
|
-
* is nothing like the status that produced it, and need not be
|
|
21
|
+
* is nothing like the status that produced it, and need not be a portable `ApiError` at all.
|
|
22
22
|
*/
|
|
23
23
|
class TranslatedFailure {
|
|
24
24
|
error;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TranslatedFailure.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/TranslatedFailure.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,iBAAiB;IAGN;IAUA;IAEA;IAdpB;IACI,sFAAsF;IACtE,KAAY;IAC5B;;;;;;;;OAQG;IACa,aAAsB;IACtC,wEAAwE;IACxD,UAAkB;QAZlB,UAAK,GAAL,KAAK,CAAO;QAUZ,kBAAa,GAAb,aAAa,CAAS;QAEtB,eAAU,GAAV,UAAU,CAAQ;IACnC,CAAC;CACP;AAjBD,8CAiBC","sourcesContent":["/**\n * What {@link ClientErrorTranslator.translateError} decided about ONE non-2xx downstream response —\n * the typed error, plus WHO decided it.\n *\n * DATA ONLY (no behavior), so it is a class with an explicit constructor rather than an interface,\n * exactly like {@link RequestOutcome}.\n *\n * WHY IT CARRIES `appRegistered` AT ALL: the translated error alone is not enough for an environment\n * hook to act on. `
|
|
1
|
+
{"version":3,"file":"TranslatedFailure.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/TranslatedFailure.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,iBAAiB;IAGN;IAUA;IAEA;IAdpB;IACI,sFAAsF;IACtE,KAAY;IAC5B;;;;;;;;OAQG;IACa,aAAsB;IACtC,wEAAwE;IACxD,UAAkB;QAZlB,UAAK,GAAL,KAAK,CAAO;QAUZ,kBAAa,GAAb,aAAa,CAAS;QAEtB,eAAU,GAAV,UAAU,CAAQ;IACnC,CAAC;CACP;AAjBD,8CAiBC","sourcesContent":["/**\n * What {@link ClientErrorTranslator.translateError} decided about ONE non-2xx downstream response —\n * the typed error, plus WHO decided it.\n *\n * DATA ONLY (no behavior), so it is a class with an explicit constructor rather than an interface,\n * exactly like {@link RequestOutcome}.\n *\n * WHY IT CARRIES `appRegistered` AT ALL: the translated error alone is not enough for an environment\n * hook to act on. `ApiNotFoundError` produced by the BUILT-IN 404 branch and `ApiNotFoundError`\n * produced by an app's own `ErrorTranslators` are indistinguishable as values, yet they mean opposite\n * things — the first is the framework's generic default, the second is the app saying out loud, at\n * startup and greppably, \"relay this status as my own\". `ProxyClient.adaptDownstreamFailure` must\n * honour the second and is free to replace the first, so the provenance has to travel WITH the error\n * rather than be re-derived by consulting `ClientRegistry` a second time.\n *\n * `statusCode` is the status the DOWNSTREAM answered — carried explicitly rather than read back off\n * `error.code`, because an app-registered translation may legitimately return an error whose `code`\n * is nothing like the status that produced it, and need not be a portable `ApiError` at all.\n */\nexport class TranslatedFailure {\n constructor(\n /** The typed error the translator picked for this response. Always a real `Error`. */\n public readonly error: Error,\n /**\n * True when an app-registered `ClientRegistry` translation claimed this status — i.e. the app\n * chose this error type deliberately, at startup, in one greppable place. False when the\n * framework's built-in status mapping produced it.\n *\n * This IS the caller's explicit opt-out from any environment-specific rewrite: see\n * `NodeProxyClient.adaptDownstreamFailure`, where an app-registered 4xx wins over the\n * server-to-server 4xx-to-500 wrap.\n */\n public readonly appRegistered: boolean,\n /** The HTTP status the downstream dependency actually answered with. */\n public readonly statusCode: number,\n ) {}\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UnexpectedApiResponseError = void 0;
|
|
4
|
+
/** HTTP adapter failure for an unrecognized status not claimed by an app translator. */
|
|
5
|
+
class UnexpectedApiResponseError extends Error {
|
|
6
|
+
statusCode;
|
|
7
|
+
constructor(statusCode, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.statusCode = statusCode;
|
|
10
|
+
this.name = 'UnexpectedApiResponseError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.UnexpectedApiResponseError = UnexpectedApiResponseError;
|
|
14
|
+
//# sourceMappingURL=UnexpectedApiResponseError.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UnexpectedApiResponseError.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/UnexpectedApiResponseError.ts"],"names":[],"mappings":";;;AAAA,wFAAwF;AACxF,MAAa,0BAA2B,SAAQ,KAAK;IAE7B;IADpB,YACoB,UAAkB,EAClC,OAAe;QAEf,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,eAAU,GAAV,UAAU,CAAQ;QAIlC,IAAI,CAAC,IAAI,GAAG,4BAA4B,CAAC;IAC7C,CAAC;CACJ;AARD,gEAQC","sourcesContent":["/** HTTP adapter failure for an unrecognized status not claimed by an app translator. */\nexport class UnexpectedApiResponseError extends Error {\n constructor(\n public readonly statusCode: number,\n message: string,\n ) {\n super(message);\n this.name = 'UnexpectedApiResponseError';\n }\n}\n"]}
|
package/src/index.d.ts
CHANGED
|
@@ -29,6 +29,7 @@ export { RequestOutcome } from './RequestOutcome';
|
|
|
29
29
|
export type { ApiPrototype } from './ApiPrototype';
|
|
30
30
|
export { buildClientProxy } from './buildClientProxy';
|
|
31
31
|
export { ClientErrorTranslator } from './ClientErrorTranslator';
|
|
32
|
+
export { UnexpectedApiResponseError } from './UnexpectedApiResponseError';
|
|
32
33
|
export { HttpResponseDtoFactory } from './HttpResponseDtoFactory';
|
|
33
34
|
export { TranslatedFailure } from './TranslatedFailure';
|
|
34
35
|
export { ResponseBodyReader } from './ResponseBodyReader';
|
package/src/index.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* a browser bundle, and nothing browser-only (a ContextReader store) reaches a server.
|
|
27
27
|
*/
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
-
exports.ClientFilterDefinition = exports.ClientRequest = exports.ResponseBodyReader = exports.TranslatedFailure = exports.HttpResponseDtoFactory = exports.ClientErrorTranslator = exports.buildClientProxy = exports.RequestOutcome = exports.ProxyClient = void 0;
|
|
29
|
+
exports.ClientFilterDefinition = exports.ClientRequest = exports.ResponseBodyReader = exports.TranslatedFailure = exports.HttpResponseDtoFactory = exports.UnexpectedApiResponseError = exports.ClientErrorTranslator = exports.buildClientProxy = exports.RequestOutcome = exports.ProxyClient = void 0;
|
|
30
30
|
var ProxyClient_1 = require("./ProxyClient");
|
|
31
31
|
Object.defineProperty(exports, "ProxyClient", { enumerable: true, get: function () { return ProxyClient_1.ProxyClient; } });
|
|
32
32
|
var RequestOutcome_1 = require("./RequestOutcome");
|
|
@@ -35,6 +35,8 @@ var buildClientProxy_1 = require("./buildClientProxy");
|
|
|
35
35
|
Object.defineProperty(exports, "buildClientProxy", { enumerable: true, get: function () { return buildClientProxy_1.buildClientProxy; } });
|
|
36
36
|
var ClientErrorTranslator_1 = require("./ClientErrorTranslator");
|
|
37
37
|
Object.defineProperty(exports, "ClientErrorTranslator", { enumerable: true, get: function () { return ClientErrorTranslator_1.ClientErrorTranslator; } });
|
|
38
|
+
var UnexpectedApiResponseError_1 = require("./UnexpectedApiResponseError");
|
|
39
|
+
Object.defineProperty(exports, "UnexpectedApiResponseError", { enumerable: true, get: function () { return UnexpectedApiResponseError_1.UnexpectedApiResponseError; } });
|
|
38
40
|
// The CLIENT-side transport boundary: a fetch Response becomes the ONE HttpResponseDto an app's
|
|
39
41
|
// ErrorTranslators sees, so node and browser hand `fromWire` the identical shape.
|
|
40
42
|
var HttpResponseDtoFactory_1 = require("./HttpResponseDtoFactory");
|
package/src/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;;AAEH,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAEvB,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA;AAC9B,gGAAgG;AAChG,kFAAkF;AAClF,mEAAkE;AAAzD,gIAAA,sBAAsB,OAAA;AAC/B,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,2DAA0D;AAAjD,wHAAA,kBAAkB,OAAA;AAC3B,kGAAkG;AAClG,kFAAkF;AAClF,gEAAgE;AAChE,iDAAgD;AAAvC,8GAAA,aAAa,OAAA;AACtB,+CAAwD;AAA/C,sHAAA,sBAAsB,OAAA","sourcesContent":["/**\n * @webpieces/http-client-core\n *\n * The ISOMORPHIC core of the webpieces HTTP client — everything that reads an API contract's\n * decorators and turns a method call into an HTTP request, with no opinion about where the\n * magic context comes from or whether a DI container exists.\n *\n * You almost certainly want one of its two environment packages instead:\n * - Server: @webpieces/http-client-node (inversify-wired, reads RequestContext, mints OIDC)\n * - Browser: @webpieces/http-client-browser (no DI — React or Angular, app-managed context store)\n *\n * Architecture:\n * ```\n * http-api (defines the contract)\n * ^\n * +-- http-routing (server: contract -> handlers)\n * +-- http-client-core (contract -> HTTP requests) <- YOU ARE HERE\n * +-- http-client-node (RequestContext + Secrets + OIDC + inversify factory)\n * +-- http-client-browser (app-held store + plain factory, no DI)\n * ```\n *\n * There is no context/credential/recording seam here at all: ProxyClient is ABSTRACT and asks its\n * subclass for the base URL, the context headers, the log map, the outbound credential, and the\n * recorder. Nothing server-only (RequestContext, Secrets, mintIdToken, TestCaseRecorder) can reach\n * a browser bundle, and nothing browser-only (a ContextReader store) reaches a server.\n */\n\nexport { ProxyClient } from './ProxyClient';\nexport { RequestOutcome } from './RequestOutcome';\nexport type { ApiPrototype } from './ApiPrototype';\nexport { buildClientProxy } from './buildClientProxy';\nexport { ClientErrorTranslator } from './ClientErrorTranslator';\n// The CLIENT-side transport boundary: a fetch Response becomes the ONE HttpResponseDto an app's\n// ErrorTranslators sees, so node and browser hand `fromWire` the identical shape.\nexport { HttpResponseDtoFactory } from './HttpResponseDtoFactory';\nexport { TranslatedFailure } from './TranslatedFailure';\nexport { ResponseBodyReader } from './ResponseBodyReader';\n// The OUTBOUND filter chain: the mutable request a filter edits, and one registration of a filter\n// at a priority. The `Filter`/`Service`/`FilterChain` abstraction itself lives in\n// @webpieces/core-util, shared with the server's inbound chain.\nexport { ClientRequest } from './ClientRequest';\nexport { ClientFilterDefinition } from './ClientFilter';\nexport type { ClientFilter } from './ClientFilter';\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../packages/http/http-client-core/src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;;;AAEH,6CAA4C;AAAnC,0GAAA,WAAW,OAAA;AACpB,mDAAkD;AAAzC,gHAAA,cAAc,OAAA;AAEvB,uDAAsD;AAA7C,oHAAA,gBAAgB,OAAA;AACzB,iEAAgE;AAAvD,8HAAA,qBAAqB,OAAA;AAC9B,2EAA0E;AAAjE,wIAAA,0BAA0B,OAAA;AACnC,gGAAgG;AAChG,kFAAkF;AAClF,mEAAkE;AAAzD,gIAAA,sBAAsB,OAAA;AAC/B,yDAAwD;AAA/C,sHAAA,iBAAiB,OAAA;AAC1B,2DAA0D;AAAjD,wHAAA,kBAAkB,OAAA;AAC3B,kGAAkG;AAClG,kFAAkF;AAClF,gEAAgE;AAChE,iDAAgD;AAAvC,8GAAA,aAAa,OAAA;AACtB,+CAAwD;AAA/C,sHAAA,sBAAsB,OAAA","sourcesContent":["/**\n * @webpieces/http-client-core\n *\n * The ISOMORPHIC core of the webpieces HTTP client — everything that reads an API contract's\n * decorators and turns a method call into an HTTP request, with no opinion about where the\n * magic context comes from or whether a DI container exists.\n *\n * You almost certainly want one of its two environment packages instead:\n * - Server: @webpieces/http-client-node (inversify-wired, reads RequestContext, mints OIDC)\n * - Browser: @webpieces/http-client-browser (no DI — React or Angular, app-managed context store)\n *\n * Architecture:\n * ```\n * http-api (defines the contract)\n * ^\n * +-- http-routing (server: contract -> handlers)\n * +-- http-client-core (contract -> HTTP requests) <- YOU ARE HERE\n * +-- http-client-node (RequestContext + Secrets + OIDC + inversify factory)\n * +-- http-client-browser (app-held store + plain factory, no DI)\n * ```\n *\n * There is no context/credential/recording seam here at all: ProxyClient is ABSTRACT and asks its\n * subclass for the base URL, the context headers, the log map, the outbound credential, and the\n * recorder. Nothing server-only (RequestContext, Secrets, mintIdToken, TestCaseRecorder) can reach\n * a browser bundle, and nothing browser-only (a ContextReader store) reaches a server.\n */\n\nexport { ProxyClient } from './ProxyClient';\nexport { RequestOutcome } from './RequestOutcome';\nexport type { ApiPrototype } from './ApiPrototype';\nexport { buildClientProxy } from './buildClientProxy';\nexport { ClientErrorTranslator } from './ClientErrorTranslator';\nexport { UnexpectedApiResponseError } from './UnexpectedApiResponseError';\n// The CLIENT-side transport boundary: a fetch Response becomes the ONE HttpResponseDto an app's\n// ErrorTranslators sees, so node and browser hand `fromWire` the identical shape.\nexport { HttpResponseDtoFactory } from './HttpResponseDtoFactory';\nexport { TranslatedFailure } from './TranslatedFailure';\nexport { ResponseBodyReader } from './ResponseBodyReader';\n// The OUTBOUND filter chain: the mutable request a filter edits, and one registration of a filter\n// at a priority. The `Filter`/`Service`/`FilterChain` abstraction itself lives in\n// @webpieces/core-util, shared with the server's inbound chain.\nexport { ClientRequest } from './ClientRequest';\nexport { ClientFilterDefinition } from './ClientFilter';\nexport type { ClientFilter } from './ClientFilter';\n"]}
|