@pipelex/sdk 0.52.0 → 0.53.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -17
- package/dist/answer-reading.d.ts +70 -0
- package/dist/answer-reading.js +106 -0
- package/dist/answer-reading.js.map +1 -0
- package/dist/artifacts.js +45 -32
- package/dist/artifacts.js.map +1 -1
- package/dist/cli/present.d.ts +11 -14
- package/dist/cli/present.js +41 -37
- package/dist/cli/present.js.map +1 -1
- package/dist/cli/run.js +8 -7
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/script.d.ts +1 -1
- package/dist/cli/source.js +2 -2
- package/dist/cli/source.js.map +1 -1
- package/dist/client.d.ts +248 -218
- package/dist/client.js +538 -477
- package/dist/client.js.map +1 -1
- package/dist/codegen-check.d.ts +21 -15
- package/dist/codegen-check.js +54 -27
- package/dist/codegen-check.js.map +1 -1
- package/dist/errors.d.ts +298 -455
- package/dist/errors.js +426 -534
- package/dist/errors.js.map +1 -1
- package/dist/exact-json.d.ts +8 -12
- package/dist/exact-json.js +19 -12
- package/dist/exact-json.js.map +1 -1
- package/dist/hooks/check-core.d.ts +12 -7
- package/dist/hooks/check-core.js +20 -14
- package/dist/hooks/check-core.js.map +1 -1
- package/dist/hooks/validate-stage.js +12 -3
- package/dist/hooks/validate-stage.js.map +1 -1
- package/dist/index.d.ts +4 -5
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/method-selector.js +7 -5
- package/dist/method-selector.js.map +1 -1
- package/dist/method-trees.d.ts +6 -7
- package/dist/method-trees.js +31 -37
- package/dist/method-trees.js.map +1 -1
- package/dist/models.d.ts +33 -210
- package/dist/models.js +2 -1
- package/dist/models.js.map +1 -1
- package/dist/prepare-inputs.d.ts +2 -2
- package/dist/prepare-inputs.js +51 -54
- package/dist/prepare-inputs.js.map +1 -1
- package/dist/problem-items.d.ts +19 -0
- package/dist/problem-items.js +52 -0
- package/dist/problem-items.js.map +1 -0
- package/dist/product-models.d.ts +23 -21
- package/dist/records.d.ts +12 -0
- package/dist/records.js +23 -0
- package/dist/records.js.map +1 -0
- package/dist/runs.d.ts +49 -38
- package/dist/runs.js +45 -19
- package/dist/runs.js.map +1 -1
- package/dist/transport-reading.d.ts +18 -0
- package/dist/{run-may-have-started.js → transport-reading.js} +15 -44
- package/dist/transport-reading.js.map +1 -0
- package/dist/upload-grant.d.ts +9 -6
- package/dist/upload-grant.js +42 -29
- package/dist/upload-grant.js.map +1 -1
- package/dist/upload.js +53 -33
- package/dist/upload.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/architecture.md +19 -19
- package/docs/artifact-download.md +6 -6
- package/docs/cli.md +1 -1
- package/docs/crate-routes.md +13 -14
- package/docs/errors.md +154 -267
- package/docs/exact-integers.md +1 -1
- package/docs/files.md +13 -13
- package/docs/hook-bundle.md +15 -1
- package/docs/input-preparation.md +14 -14
- package/docs/method-copy.md +24 -24
- package/docs/method-examples.md +13 -14
- package/docs/method-trees.md +27 -28
- package/docs/organization-branding.md +14 -16
- package/docs/run-results.md +8 -8
- package/package.json +1 -1
- package/dist/error-models.d.ts +0 -302
- package/dist/error-models.js +0 -203
- package/dist/error-models.js.map +0 -1
- package/dist/error-verdicts.d.ts +0 -46
- package/dist/error-verdicts.js +0 -120
- package/dist/error-verdicts.js.map +0 -1
- package/dist/run-may-have-started.d.ts +0 -31
- package/dist/run-may-have-started.js.map +0 -1
package/dist/errors.js
CHANGED
|
@@ -1,99 +1,112 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pipelex SDK errors — the argument, transport, run-lifecycle, input-preparation and artifact
|
|
3
|
-
* errors `PipelexApiClient` raises. Every one derives from `PipelexRequestError`, itself
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
function
|
|
21
|
-
return
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
* `ApiResponseError` built for a request that may create a run, and `PipelineExecuteTimeoutError`,
|
|
31
|
-
* ever carry it as true, and when they do, `retryable` is false whatever the verdict passed.
|
|
32
|
-
*
|
|
33
|
-
* It refines the standard's `PipelineRequestError`, so `instanceof PipelineRequestError` still
|
|
34
|
-
* matches every SDK error. To read a verdict from anything a `catch` holds, use
|
|
35
|
-
* `errorVerdictOf`, which also reads it off an error raised by another copy of this SDK, and
|
|
36
|
-
* `runMayHaveStartedOf` for the third member.
|
|
37
|
-
*/
|
|
38
|
-
export class PipelexRequestError extends PipelineRequestError {
|
|
39
|
-
/** Whether asking again can plausibly succeed. Always decided. */
|
|
40
|
-
retryable;
|
|
41
|
-
/** Who can fix the failure: `input`, `config` or `runtime`. Always decided. */
|
|
42
|
-
errorDomain;
|
|
43
|
-
/**
|
|
44
|
-
* Whether the failed request may still have started a run whose id never reached the caller:
|
|
45
|
-
* a `start` or an `execute` (and so `startAndWaitForResult`'s start, or the blocking execute it
|
|
46
|
-
* falls back to) that failed in a way that does not prove the server started nothing. When it
|
|
47
|
-
* is true, `retryable` is false: look the run up in the run history before starting it again.
|
|
48
|
-
* False for every other request, a wait on a run already named included. Always decided.
|
|
49
|
-
*/
|
|
50
|
-
runMayHaveStarted;
|
|
51
|
-
constructor(message, verdict, options) {
|
|
52
|
-
super(message, options);
|
|
53
|
-
this.name = "PipelexRequestError";
|
|
54
|
-
this.runMayHaveStarted = options?.runMayHaveStarted === true;
|
|
55
|
-
// Asking again could start a second run, so a run that may have started is never retryable.
|
|
56
|
-
this.retryable = this.runMayHaveStarted ? false : verdict.retryable;
|
|
57
|
-
this.errorDomain = verdict.errorDomain;
|
|
58
|
-
}
|
|
3
|
+
* errors `PipelexApiClient` raises. Every one derives from `PipelexRequestError`, itself the MTHDS
|
|
4
|
+
* standard's `ProblemError` (re-exported from `mthds/protocol`), and so carries its failure as a
|
|
5
|
+
* problem document: the runtime's own when it answered with one, and otherwise the problem the SDK
|
|
6
|
+
* builds for a failure it detects itself, under a registered code. A program branches on `code`,
|
|
7
|
+
* and on `domain`, `retry` and `nextStep` when it handles several codes alike; the rest of the
|
|
8
|
+
* problem (`params`, `location`, `errors`, `pipeline_run_id`, `request_id` and any member the
|
|
9
|
+
* runtime added) is read off `problem`. The Python twin, `pipelex-sdk`, builds the same code for
|
|
10
|
+
* every failure, which the shared case file `tests/fixtures/sdk-problems.json` pins in both suites.
|
|
11
|
+
*
|
|
12
|
+
* The exported errors outside the family, `CodegenLockError` and `ExactJsonUnsupportedError`, are
|
|
13
|
+
* `ProblemError`s too.
|
|
14
|
+
*/
|
|
15
|
+
import { ProblemError, buildProblem, isProblem, readAnswer, readProblemBody } from "mthds/protocol";
|
|
16
|
+
import { outcomeUnknownProblem, readHostedAnswer } from "./answer-reading.js";
|
|
17
|
+
import { requestMayHaveLeft } from "./transport-reading.js";
|
|
18
|
+
export { ProblemError };
|
|
19
|
+
/** The `cause` option of an error, given only when there is one, so `cause` stays absent otherwise. */
|
|
20
|
+
function causeOption(cause) {
|
|
21
|
+
return cause === undefined ? undefined : { cause };
|
|
22
|
+
}
|
|
23
|
+
// ── The problems this module builds ──────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* The problem of an argument refused before any request left: `client.argument_invalid`, naming
|
|
26
|
+
* the argument and why it was refused, as an identifier (`not_a_string`, `mutually_exclusive`, …).
|
|
27
|
+
*/
|
|
28
|
+
export function argumentInvalidProblem(argument, reason) {
|
|
29
|
+
return buildProblem("client.argument_invalid", { argument, reason });
|
|
59
30
|
}
|
|
60
31
|
/**
|
|
61
|
-
* The
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*/
|
|
75
|
-
export function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
32
|
+
* The problem of an answer the SDK cannot use as the route's answer: `client.response_unreadable`,
|
|
33
|
+
* naming its status, and the run it is about as its member when there is one.
|
|
34
|
+
*/
|
|
35
|
+
export function responseUnreadableProblem(status, runId) {
|
|
36
|
+
return buildProblem("client.response_unreadable", { status }, runId === undefined ? {} : { pipeline_run_id: runId });
|
|
37
|
+
}
|
|
38
|
+
/** The problem the standard reads from an answer's status alone, as for a body that is no problem document. */
|
|
39
|
+
export function statusProblem(status) {
|
|
40
|
+
return readAnswer({ status, headers: {}, body: "" }, { createsRun: false }).problem;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether a storage answer's `status` refuses the request for its timing or fails in a way a later
|
|
44
|
+
* attempt can pass: a `408`, a `429`, or a `5xx` other than `501`.
|
|
45
|
+
*/
|
|
46
|
+
export function isTransientStorageStatus(status) {
|
|
47
|
+
return status === 408 || status === 429 || (status >= 500 && status <= 599 && status !== 501);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The problem of a storage answer the SDK reads by its status, storage answering no problem
|
|
51
|
+
* document: `runtime.unavailable` for a transient status ({@link isTransientStorageStatus}), so
|
|
52
|
+
* that asking again can succeed; `runtime.unsupported` for a `501`, storage not implementing the
|
|
53
|
+
* request it was sent; and `client.response_unreadable` naming the status otherwise, an answer the
|
|
54
|
+
* SDK has no reading for.
|
|
55
|
+
*/
|
|
56
|
+
export function storageStatusProblem(status) {
|
|
57
|
+
if (isTransientStorageStatus(status))
|
|
58
|
+
return buildProblem("runtime.unavailable", {});
|
|
59
|
+
if (status === 501)
|
|
60
|
+
return buildProblem("runtime.unsupported", { capability: "this storage request" });
|
|
61
|
+
return responseUnreadableProblem(status);
|
|
62
|
+
}
|
|
63
|
+
/** The problem of a request refused for its credential: `auth.invalid_credential`, or `auth.missing_credential` when it carried none. */
|
|
64
|
+
function credentialRefusedProblem(credentialSent) {
|
|
65
|
+
return credentialSent
|
|
66
|
+
? buildProblem("auth.invalid_credential", {})
|
|
67
|
+
: buildProblem("auth.missing_credential", {});
|
|
68
|
+
}
|
|
69
|
+
/** The problem an error wrapping another carries: the wrapped error's, when it carries one. */
|
|
70
|
+
function wrappedProblem(cause) {
|
|
71
|
+
return cause instanceof ProblemError ? cause.problem : undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The problem of a refused credential, in this order: the server's own, when the wrapped
|
|
75
|
+
* `ApiResponseError` answered a problem document; then what the caller knows of its own request,
|
|
76
|
+
* when it says whether the request carried a credential, which the edge's bare `401` or `403` does
|
|
77
|
+
* not say; then the wrapped error's problem, its reading of that status; and otherwise
|
|
78
|
+
* `auth.invalid_credential`.
|
|
79
|
+
*/
|
|
80
|
+
function credentialProblem(cause, credentialSent) {
|
|
81
|
+
if (cause instanceof ApiResponseError && readProblemBody(cause.responseBody) !== undefined) {
|
|
82
|
+
return cause.problem;
|
|
81
83
|
}
|
|
82
|
-
|
|
84
|
+
if (credentialSent !== undefined)
|
|
85
|
+
return credentialRefusedProblem(credentialSent);
|
|
86
|
+
return wrappedProblem(cause) ?? credentialRefusedProblem(true);
|
|
83
87
|
}
|
|
88
|
+
// ── The base every request error derives from ────────────────────────
|
|
84
89
|
/**
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
90
|
+
* The base of every error this SDK raises over a request, carrying the failure as `problem`.
|
|
91
|
+
*
|
|
92
|
+
* It refines the standard's `ProblemError`, so every getter that reads the problem is there:
|
|
93
|
+
* `code`, the member a program branches on; `domain`, who must act (`input`, `method`,
|
|
94
|
+
* `account`, `operator` or `runtime`); `retry`, whether sending the same request again can succeed
|
|
95
|
+
* (`yes`, `no`, or `outcome_unknown` where a run may already exist and must be read, never started
|
|
96
|
+
* again); `nextStep`, what to do; `retryAfter`, the seconds to wait before a repeat when the
|
|
97
|
+
* problem gives them; and `requestId`, the id to hand to support. `instanceof ProblemError` and
|
|
98
|
+
* `instanceof PipelineRequestError` match every SDK error, as does `mthds`'s `isProblemError`.
|
|
99
|
+
*
|
|
100
|
+
* The message is the SDK's own sentence, as it always was, rather than the problem's: the problem's
|
|
101
|
+
* `detail` and `next_step.detail` are on `problem` for whoever renders them. Its subclasses are
|
|
102
|
+
* meant to be raised, never this class itself.
|
|
92
103
|
*/
|
|
93
|
-
export
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
104
|
+
export class PipelexRequestError extends ProblemError {
|
|
105
|
+
constructor(message, problem, options) {
|
|
106
|
+
super(problem, options);
|
|
107
|
+
this.name = "PipelexRequestError";
|
|
108
|
+
this.message = message;
|
|
109
|
+
}
|
|
97
110
|
}
|
|
98
111
|
// ── Argument refusals ────────────────────────────────────────────────
|
|
99
112
|
/**
|
|
@@ -107,15 +120,16 @@ export function runMayHaveStartedOf(err) {
|
|
|
107
120
|
* to 255 visible ASCII characters, or a base URL that is not host-only. Nothing reached the API,
|
|
108
121
|
* so the message says what to change.
|
|
109
122
|
*
|
|
110
|
-
* Its
|
|
111
|
-
* `
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
123
|
+
* Its problem is `client.argument_invalid`, unless `options.problem` gives another: the client
|
|
124
|
+
* gives `client.not_configured` naming `base_url` for a base URL that is not host-only, since it
|
|
125
|
+
* typically comes from `PIPELEX_BASE_URL`. One built from a message alone names the argument
|
|
126
|
+
* `arguments`, for the reason `unspecified`. A refusal the standard's own check raises
|
|
127
|
+
* (`assertExclusiveRunSources`) is rethrown as this class with the same message, the standard's
|
|
128
|
+
* error as `cause`.
|
|
115
129
|
*/
|
|
116
130
|
export class RequestArgumentError extends PipelexRequestError {
|
|
117
131
|
constructor(message, options) {
|
|
118
|
-
super(message, options?.
|
|
132
|
+
super(message, options?.problem ?? argumentInvalidProblem("arguments", "unspecified"), causeOption(options?.cause));
|
|
119
133
|
this.name = "RequestArgumentError";
|
|
120
134
|
}
|
|
121
135
|
}
|
|
@@ -126,25 +140,34 @@ export class RequestArgumentError extends PipelexRequestError {
|
|
|
126
140
|
* subclass to branch on the semantic category. All preparation failures are
|
|
127
141
|
* raised BEFORE any run is created — a run never triggers a hidden upload.
|
|
128
142
|
*
|
|
129
|
-
* Its
|
|
130
|
-
*
|
|
131
|
-
* consumer's, such as a form reporting a missing input in
|
|
143
|
+
* Its problem is the one `options.problem` gives: the SDK's own subclasses state theirs, a direct
|
|
144
|
+
* raise gives `client.argument_invalid` for an argument refused or the wrapped error's for a
|
|
145
|
+
* refusal the API answered, and a consumer's subclass, such as a form reporting a missing input in
|
|
146
|
+
* its own words, may give any. One built with none, from a message alone, carries
|
|
147
|
+
* `client.argument_invalid` naming `inputs`, for the reason `unspecified`.
|
|
132
148
|
*/
|
|
133
149
|
export class InputPreparationError extends PipelexRequestError {
|
|
134
150
|
constructor(message, options) {
|
|
135
|
-
super(message, options?.
|
|
151
|
+
super(message, options?.problem ?? argumentInvalidProblem("inputs", "unspecified"), causeOption(options?.cause));
|
|
136
152
|
this.name = "InputPreparationError";
|
|
137
153
|
}
|
|
138
154
|
}
|
|
139
155
|
/**
|
|
140
156
|
* A local asset could not be turned into bytes: a missing or unreadable path, or
|
|
141
157
|
* a path string in a non-Node runtime (path strings are Node-only). `source` is
|
|
142
|
-
* the offending path.
|
|
158
|
+
* the offending path. Its problem is `input.file_unreadable`, naming the path.
|
|
143
159
|
*/
|
|
144
160
|
export class InvalidLocalSourceError extends InputPreparationError {
|
|
145
161
|
source;
|
|
146
162
|
constructor(message, source, options) {
|
|
147
|
-
super(message,
|
|
163
|
+
super(message, {
|
|
164
|
+
...options,
|
|
165
|
+
problem: buildProblem("input.file_unreadable", {
|
|
166
|
+
input: source,
|
|
167
|
+
file_type: "file",
|
|
168
|
+
reason: "unreadable",
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
148
171
|
this.name = "InvalidLocalSourceError";
|
|
149
172
|
this.source = source;
|
|
150
173
|
}
|
|
@@ -160,13 +183,20 @@ export class InvalidLocalSourceError extends InputPreparationError {
|
|
|
160
183
|
* As on `UploadNotAllowedError`, `inputName` is the input the value belongs to and `inputPath`
|
|
161
184
|
* where it sits inside that input (`report.attachments[1]`, or the name alone for a top-level
|
|
162
185
|
* file). The message names that path and describes the value by its kind, never by its content,
|
|
163
|
-
* since the value is the caller's data.
|
|
186
|
+
* since the value is the caller's data. Its problem is `input.invalid`, naming the input and the
|
|
187
|
+
* path inside it.
|
|
164
188
|
*/
|
|
165
189
|
export class InvalidInputValueError extends InputPreparationError {
|
|
166
190
|
inputName;
|
|
167
191
|
inputPath;
|
|
168
192
|
constructor(message, details, options) {
|
|
169
|
-
super(message,
|
|
193
|
+
super(message, {
|
|
194
|
+
...options,
|
|
195
|
+
problem: buildProblem("input.invalid", {
|
|
196
|
+
input: details.inputName,
|
|
197
|
+
field: details.inputPath,
|
|
198
|
+
}),
|
|
199
|
+
});
|
|
170
200
|
this.name = "InvalidInputValueError";
|
|
171
201
|
this.inputName = details.inputName;
|
|
172
202
|
this.inputPath = details.inputPath;
|
|
@@ -184,8 +214,9 @@ const UPLOAD_NOT_ALLOWED_KIND_WORDS = {
|
|
|
184
214
|
* reads a file of its own because the caller named it. `inputName` is the input the value belongs
|
|
185
215
|
* to, `inputPath` where it sits inside that input (`report.attachments[1]`, or the name alone for a
|
|
186
216
|
* top-level file), and `valueKind` what it is. The value itself is neither in the message nor on
|
|
187
|
-
* the error, since it is the caller's data. Its
|
|
188
|
-
* pass an `http(s)` URL or a
|
|
217
|
+
* the error, since it is the caller's data. Its problem is `client.argument_invalid`, naming the
|
|
218
|
+
* input's path and the value's kind as the reason: the caller must pass an `http(s)` URL or a
|
|
219
|
+
* `pipelex-storage://` reference instead.
|
|
189
220
|
*/
|
|
190
221
|
export class UploadNotAllowedError extends InputPreparationError {
|
|
191
222
|
inputName;
|
|
@@ -194,7 +225,7 @@ export class UploadNotAllowedError extends InputPreparationError {
|
|
|
194
225
|
constructor(details, options) {
|
|
195
226
|
super(`Input "${details.inputPath}" is ${UPLOAD_NOT_ALLOWED_KIND_WORDS[details.valueKind]}, which ` +
|
|
196
227
|
"would need an upload, and this preparation uploads nothing: pass an http(s) URL or a " +
|
|
197
|
-
"pipelex-storage:// reference instead.", options);
|
|
228
|
+
"pipelex-storage:// reference instead.", { ...options, problem: argumentInvalidProblem(details.inputPath, details.valueKind) });
|
|
198
229
|
this.name = "UploadNotAllowedError";
|
|
199
230
|
this.inputName = details.inputName;
|
|
200
231
|
this.inputPath = details.inputPath;
|
|
@@ -203,18 +234,19 @@ export class UploadNotAllowedError extends InputPreparationError {
|
|
|
203
234
|
}
|
|
204
235
|
/**
|
|
205
236
|
* The pipe I/O answer said the method does not load (`is_valid: false`), so its signature cannot
|
|
206
|
-
* be read and no input can be prepared: the method, not the inputs, must change.
|
|
207
|
-
* `
|
|
208
|
-
* `
|
|
209
|
-
* error's message names the first item's
|
|
237
|
+
* be read and no input can be prepared: the method, not the inputs, must change. Its problem is
|
|
238
|
+
* `method.invalid`, whose `errors` are the answer's problem items, each read member by member
|
|
239
|
+
* (see `readProblemItems`), possibly none; `serverMessage` is the answer's own `message` when it is
|
|
240
|
+
* a string. The error's message names the first item's `detail`, else the answer's own message.
|
|
210
241
|
*/
|
|
211
242
|
export class MethodLoadError extends InputPreparationError {
|
|
212
|
-
validationErrors;
|
|
213
243
|
serverMessage;
|
|
214
244
|
constructor(message, details, options) {
|
|
215
|
-
super(message,
|
|
245
|
+
super(message, {
|
|
246
|
+
...options,
|
|
247
|
+
problem: buildProblem("method.invalid", {}, { errors: details.errors }),
|
|
248
|
+
});
|
|
216
249
|
this.name = "MethodLoadError";
|
|
217
|
-
this.validationErrors = details.validationErrors;
|
|
218
250
|
this.serverMessage = details.serverMessage;
|
|
219
251
|
}
|
|
220
252
|
}
|
|
@@ -225,48 +257,63 @@ export class MethodLoadError extends InputPreparationError {
|
|
|
225
257
|
* time out or throttle the request, a `400 RequestTimeout` and a `409
|
|
226
258
|
* ConditionalRequestConflict` are an `UploadTransportError`. The SDK
|
|
227
259
|
* does not impose a client-side cap; it surfaces the refusal. `filename` and
|
|
228
|
-
* `status` locate it, and `
|
|
260
|
+
* `status` locate it, and `reason` says why: the SDK sets it on every one it raises,
|
|
229
261
|
* so it is undefined only on one a caller constructs without it.
|
|
262
|
+
*
|
|
263
|
+
* Its problem is the refusal's as the API answered it, which `uploadFile` passes, and otherwise
|
|
264
|
+
* the one the standard reads from `status` alone, storage answering no problem document.
|
|
230
265
|
*/
|
|
231
266
|
export class RejectedAssetError extends InputPreparationError {
|
|
232
267
|
filename;
|
|
233
268
|
status;
|
|
234
|
-
|
|
269
|
+
reason;
|
|
235
270
|
constructor(message, filename, status, options) {
|
|
236
|
-
super(message,
|
|
271
|
+
super(message, {
|
|
272
|
+
cause: options?.cause,
|
|
273
|
+
problem: options?.problem ?? statusProblem(status),
|
|
274
|
+
});
|
|
237
275
|
this.name = "RejectedAssetError";
|
|
238
276
|
this.filename = filename;
|
|
239
277
|
this.status = status;
|
|
240
|
-
this.
|
|
278
|
+
this.reason = options?.reason;
|
|
241
279
|
}
|
|
242
280
|
}
|
|
243
281
|
/**
|
|
244
282
|
* The configured deployment does not support upload (no `/v1/upload` route, seen
|
|
245
283
|
* as a `404` other than a test file's unknown method, which is a {@link TestFileRefusedError}).
|
|
246
284
|
* Upload is a hosted Pipelex-product capability even though the SDK
|
|
247
|
-
* can be pointed at other base URLs. Its
|
|
248
|
-
*
|
|
249
|
-
* `uploadFile` sent it.
|
|
285
|
+
* can be pointed at other base URLs. Its problem is `runtime.unsupported`, naming file upload as
|
|
286
|
+
* the capability the deployment lacks: the base URL must point at a deployment that serves
|
|
287
|
+
* upload. `filename` is the file whose upload met it, as `uploadFile` sent it.
|
|
250
288
|
*/
|
|
251
289
|
export class UnsupportedUploadCapabilityError extends InputPreparationError {
|
|
252
290
|
filename;
|
|
253
291
|
constructor(message, options) {
|
|
254
|
-
super(message, {
|
|
292
|
+
super(message, {
|
|
293
|
+
cause: options?.cause,
|
|
294
|
+
problem: buildProblem("runtime.unsupported", { capability: "file upload" }),
|
|
295
|
+
});
|
|
255
296
|
this.name = "UnsupportedUploadCapabilityError";
|
|
256
297
|
this.filename = options?.filename;
|
|
257
298
|
}
|
|
258
299
|
}
|
|
259
300
|
/**
|
|
260
301
|
* Upload was not authorized — a `401`, or a `403` from the upload route other than a test file's
|
|
261
|
-
*
|
|
262
|
-
*
|
|
302
|
+
* refused role, which is a {@link TestFileRefusedError}. Its problem is the server's own when the
|
|
303
|
+
* wrapped error, which `uploadFile` passes as `cause`, answered a problem document. Otherwise it is
|
|
304
|
+
* `auth.invalid_credential` when the request carried a credential and `auth.missing_credential`
|
|
305
|
+
* when it carried none, as `options.credentialSent` says, and the wrapped error's reading of the
|
|
306
|
+
* status when the caller does not say. `filename` is the file whose upload was refused, as
|
|
263
307
|
* `uploadFile` sent it.
|
|
264
308
|
*/
|
|
265
309
|
export class UploadAuthenticationError extends InputPreparationError {
|
|
266
310
|
status;
|
|
267
311
|
filename;
|
|
268
312
|
constructor(message, status, options) {
|
|
269
|
-
super(message, {
|
|
313
|
+
super(message, {
|
|
314
|
+
cause: options?.cause,
|
|
315
|
+
problem: credentialProblem(options?.cause, options?.credentialSent),
|
|
316
|
+
});
|
|
270
317
|
this.name = "UploadAuthenticationError";
|
|
271
318
|
this.status = status;
|
|
272
319
|
this.filename = options?.filename;
|
|
@@ -275,102 +322,114 @@ export class UploadAuthenticationError extends InputPreparationError {
|
|
|
275
322
|
/**
|
|
276
323
|
* The upload route refused a test file (`kind: "test"`) for what it claims, never for its bytes:
|
|
277
324
|
* the caller's role may not change methods, or the method it names is unknown or being erased.
|
|
278
|
-
* `
|
|
325
|
+
* `reason` says which; the SDK sets it on every one it raises, so it is undefined only on one a
|
|
279
326
|
* caller constructs without it. `status` is the HTTP status, `filename` the file `uploadFile` was
|
|
280
327
|
* sending and `methodId` the method it named, and the wrapped `ApiResponseError` is reachable via
|
|
281
328
|
* `cause`.
|
|
282
329
|
*
|
|
283
|
-
* Its
|
|
284
|
-
*
|
|
285
|
-
*
|
|
330
|
+
* Its problem is the wrapped error's when `cause` carries one, the server's own. Otherwise it is
|
|
331
|
+
* `auth.forbidden` for `role_not_permitted`, naming a role that may change methods as the
|
|
332
|
+
* requirement, and `client.argument_invalid` naming `method_id` for the other two, the reason as
|
|
333
|
+
* the reason.
|
|
286
334
|
*/
|
|
287
335
|
export class TestFileRefusedError extends InputPreparationError {
|
|
288
336
|
status;
|
|
289
|
-
|
|
337
|
+
reason;
|
|
290
338
|
filename;
|
|
291
339
|
methodId;
|
|
292
340
|
constructor(message, options) {
|
|
293
341
|
super(message, {
|
|
294
|
-
|
|
295
|
-
|
|
342
|
+
cause: options?.cause,
|
|
343
|
+
problem: wrappedProblem(options?.cause) ?? testFileRefusalProblem(options?.reason),
|
|
296
344
|
});
|
|
297
345
|
this.name = "TestFileRefusedError";
|
|
298
346
|
this.status = options?.status;
|
|
299
|
-
this.
|
|
347
|
+
this.reason = options?.reason;
|
|
300
348
|
this.filename = options?.filename;
|
|
301
349
|
this.methodId = options?.methodId;
|
|
302
350
|
}
|
|
303
351
|
}
|
|
304
|
-
/** The
|
|
305
|
-
function
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
352
|
+
/** The problem of a test file's refusal that wraps no error carrying one; `undefined` leaves the family's own. */
|
|
353
|
+
function testFileRefusalProblem(reason) {
|
|
354
|
+
switch (reason) {
|
|
355
|
+
case "role_not_permitted":
|
|
356
|
+
return buildProblem("auth.forbidden", { requirement: "a role that may change methods" });
|
|
357
|
+
case "method_not_found":
|
|
358
|
+
case "method_being_deleted":
|
|
359
|
+
return argumentInvalidProblem("method_id", reason);
|
|
360
|
+
case undefined:
|
|
361
|
+
return undefined;
|
|
362
|
+
}
|
|
310
363
|
}
|
|
311
364
|
/**
|
|
312
365
|
* A network or server fault reaching the upload route or storage — an unreachable
|
|
313
366
|
* host, a timeout, a `5xx`, a refused redirect, storage timing out on the body,
|
|
314
367
|
* storage timing out or throttling the request (a `408` or a `429`), or any other
|
|
315
|
-
* unexpected `upload()` failure. `
|
|
368
|
+
* unexpected `upload()` failure. `reason` says which: the SDK sets it on
|
|
316
369
|
* every one it raises, so it is undefined only on one a caller constructs without
|
|
317
370
|
* it. `status` is the HTTP status when a response produced it, and undefined when
|
|
318
371
|
* none did. From `uploadFile` the wrapped `ApiResponseError` is also reachable via
|
|
319
372
|
* `cause`, and `filename` is the file it was sending; `uploadWithGrant` wraps no response,
|
|
320
373
|
* because storage's error body can echo the grant's credential, and leaves `filename` undefined.
|
|
321
374
|
*
|
|
322
|
-
* Its
|
|
323
|
-
* `ApiResponseError` or `ApiUnreachableError` the client's `upload()`
|
|
324
|
-
*
|
|
325
|
-
*
|
|
326
|
-
* `storage_timeout` and `conflict` are `runtime` and retryable, `conflict` because storage
|
|
327
|
-
* documents its `409 ConditionalRequestConflict` as retryable and the grant is not spent by it (a
|
|
328
|
-
* spent hashless grant is the `412` of a `RejectedAssetError` with code `grant_used`); `server_error` is
|
|
329
|
-
* `runtime`, and retryable when the fallback table a refused API request reads would call its
|
|
330
|
-
* `status` retryable — any `5xx` but a `501`, which storage answers for a request it does not
|
|
331
|
-
* implement — or when it carries no status; `unexpected` is `runtime`, and retryable when that
|
|
332
|
-
* table would call its `status` retryable — storage's `408` or `429`, refused for its timing —
|
|
333
|
-
* and not retryable for any other status or none; `unreachable` is `config` and retryable, like
|
|
334
|
-
* `ApiUnreachableError`; `redirected` is `config` and not retryable; `invalid_grant_url` and no
|
|
335
|
-
* code at all are `runtime` and not retryable.
|
|
375
|
+
* Its problem is `options.problem` when given, else the wrapped error's when `cause` carries one:
|
|
376
|
+
* `uploadFile` wraps the `ApiResponseError` or `ApiUnreachableError` the client's `upload()`
|
|
377
|
+
* threw. Otherwise — always the case from `uploadWithGrant`, which wraps no response — `reason`
|
|
378
|
+
* decides (`uploadTransportProblem`).
|
|
336
379
|
*/
|
|
337
380
|
export class UploadTransportError extends InputPreparationError {
|
|
338
381
|
status;
|
|
339
|
-
|
|
382
|
+
reason;
|
|
340
383
|
filename;
|
|
341
384
|
constructor(message, options) {
|
|
342
385
|
super(message, {
|
|
343
|
-
|
|
344
|
-
|
|
386
|
+
cause: options?.cause,
|
|
387
|
+
problem: options?.problem ??
|
|
388
|
+
wrappedProblem(options?.cause) ??
|
|
389
|
+
uploadTransportProblem(options?.reason, options?.status),
|
|
345
390
|
});
|
|
346
391
|
this.name = "UploadTransportError";
|
|
347
392
|
this.status = options?.status;
|
|
348
|
-
this.
|
|
393
|
+
this.reason = options?.reason;
|
|
349
394
|
this.filename = options?.filename;
|
|
350
395
|
}
|
|
351
396
|
}
|
|
352
|
-
/**
|
|
353
|
-
|
|
354
|
-
|
|
397
|
+
/**
|
|
398
|
+
* The problem of an upload transport failure that wraps no error carrying one, by its reason and
|
|
399
|
+
* the status storage answered:
|
|
400
|
+
*
|
|
401
|
+
* - `timeout`, `unreachable`, `storage_timeout` and `conflict` are `runtime.unavailable`: storage,
|
|
402
|
+
* a service the upload depends on, did not take the file, and a later attempt can (the SDK's
|
|
403
|
+
* own raise sites give `client.timeout` and `client.unreachable` instead, naming the limit and
|
|
404
|
+
* the host);
|
|
405
|
+
* - `server_error`, `unexpected` and no reason at all are `storageStatusProblem`'s reading of the
|
|
406
|
+
* status: `runtime.unavailable` for a `408`, a `429` or a `5xx` other than `501`,
|
|
407
|
+
* `runtime.unsupported` for a `501`, `client.response_unreadable` for any other status; a
|
|
408
|
+
* `server_error` with no status is `runtime.unavailable`, and the others `runtime.unexpected`;
|
|
409
|
+
* - `redirected` is `runtime.misconfigured`: storage redirecting a signed upload is a fault of the
|
|
410
|
+
* deployment;
|
|
411
|
+
* - `invalid_grant_url` is `client.response_unreadable`: the grant the API answered is unusable.
|
|
412
|
+
*/
|
|
413
|
+
export function uploadTransportProblem(reason, status) {
|
|
414
|
+
switch (reason) {
|
|
355
415
|
case "timeout":
|
|
416
|
+
case "unreachable":
|
|
356
417
|
case "storage_timeout":
|
|
357
418
|
case "conflict":
|
|
358
|
-
return
|
|
419
|
+
return buildProblem("runtime.unavailable", {});
|
|
359
420
|
case "server_error":
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
421
|
+
return status === undefined
|
|
422
|
+
? buildProblem("runtime.unavailable", {})
|
|
423
|
+
: storageStatusProblem(status);
|
|
363
424
|
case "unexpected":
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
return makeVerdict("config", true);
|
|
425
|
+
case undefined:
|
|
426
|
+
return status === undefined
|
|
427
|
+
? buildProblem("runtime.unexpected", {})
|
|
428
|
+
: storageStatusProblem(status);
|
|
369
429
|
case "redirected":
|
|
370
|
-
return
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
return makeVerdict("runtime", false);
|
|
430
|
+
return buildProblem("runtime.misconfigured", {});
|
|
431
|
+
case "invalid_grant_url":
|
|
432
|
+
return responseUnreadableProblem(200);
|
|
374
433
|
}
|
|
375
434
|
}
|
|
376
435
|
/**
|
|
@@ -383,14 +442,15 @@ function uploadTransportVerdict(code, status) {
|
|
|
383
442
|
* failures on the resolve route (`ApiResponseError`, `ApiUnreachableError`)
|
|
384
443
|
* and the run-lifecycle errors propagate unchanged, so they are not subclasses.
|
|
385
444
|
*
|
|
386
|
-
* Its
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
* cannot be created
|
|
445
|
+
* Its problem is `options.problem`, else `runtime.unexpected`: the SDK gives
|
|
446
|
+
* `client.argument_invalid` where it refuses an argument (a `scope`, a bound, a location),
|
|
447
|
+
* `runtime.unsupported` for a runtime with no filesystem, `client.write_failed` for a directory
|
|
448
|
+
* that cannot be created, and `client.response_unreadable` for a resolution that answered the
|
|
449
|
+
* wrong count of items.
|
|
390
450
|
*/
|
|
391
451
|
export class ArtifactOperationError extends PipelexRequestError {
|
|
392
452
|
constructor(message, options) {
|
|
393
|
-
super(message, options?.
|
|
453
|
+
super(message, options?.problem ?? buildProblem("runtime.unexpected", {}), causeOption(options?.cause));
|
|
394
454
|
this.name = "ArtifactOperationError";
|
|
395
455
|
}
|
|
396
456
|
}
|
|
@@ -399,14 +459,14 @@ export class ArtifactOperationError extends PipelexRequestError {
|
|
|
399
459
|
* results: `main_stuff` or `working_memory` is `null`, or the key is missing
|
|
400
460
|
* from the body altogether. Distinct from an empty walk over a present scope,
|
|
401
461
|
* which is a produced verdict with no artifacts. `scope` names the scope,
|
|
402
|
-
* `runId` the run.
|
|
462
|
+
* `runId` the run. Its problem is `client.response_unreadable`, naming the run.
|
|
403
463
|
*/
|
|
404
464
|
export class ScopeUnavailableError extends ArtifactOperationError {
|
|
405
465
|
scope;
|
|
406
466
|
runId;
|
|
407
467
|
constructor(scope, runId, options) {
|
|
408
468
|
super(`Run "${runId}" carries no "${scope}" artifact to walk for produced files — ` +
|
|
409
|
-
"it is null or absent from the results body.", options);
|
|
469
|
+
"it is null or absent from the results body.", { cause: options?.cause, problem: responseUnreadableProblem(200, runId) });
|
|
410
470
|
this.name = "ScopeUnavailableError";
|
|
411
471
|
this.scope = scope;
|
|
412
472
|
this.runId = runId;
|
|
@@ -414,56 +474,70 @@ export class ScopeUnavailableError extends ArtifactOperationError {
|
|
|
414
474
|
}
|
|
415
475
|
/**
|
|
416
476
|
* One reference could not be turned into a bounded response by `fetchArtifact`.
|
|
417
|
-
* `
|
|
418
|
-
* per-item errors: the resolve route's own per-reference
|
|
477
|
+
* `reason` says why, in a closed vocabulary the download verdict shares for its
|
|
478
|
+
* per-item errors: the resolve route's own per-reference reasons
|
|
419
479
|
* (`invalid_storage_uri`, `forbidden`), then the fetch boundary's —
|
|
420
480
|
* `unsupported_url`, `plain_http_refused`, `redirect_refused`, `store_refused`
|
|
421
481
|
* (a 401/403 from the object store), `not_found` (404/410), `store_error`
|
|
422
|
-
* (any other non-2xx), `too_large`, `timeout`, `network
|
|
423
|
-
* store's HTTP status when one was received. `downloadArtifacts`
|
|
424
|
-
* this escape: it becomes the item's `error`.
|
|
425
|
-
*
|
|
426
|
-
* Its
|
|
427
|
-
*
|
|
428
|
-
* `
|
|
429
|
-
*
|
|
430
|
-
* is `runtime`, and retryable when the fallback table a refused API request reads would call its
|
|
431
|
-
* `status` retryable — a `408`, a `429` or a `5xx` other than `501` — and not retryable for any
|
|
432
|
-
* other status or none. A code this version does not know is `runtime` and not retryable.
|
|
482
|
+
* (any other non-2xx), `too_large`, `timeout`, `network`, and `unknown` for a failure the SDK
|
|
483
|
+
* cannot place. `status` is the store's HTTP status when one was received. `downloadArtifacts`
|
|
484
|
+
* never lets this escape: it becomes the item's `error`.
|
|
485
|
+
*
|
|
486
|
+
* Its problem is `options.problem` when given, else follows `reason` and `status`
|
|
487
|
+
* (`artifactFetchProblem`). The SDK gives `client.download_limit_exceeded` naming the cap for
|
|
488
|
+
* `too_large`, `client.timeout` naming the budget for `timeout`, and `client.unreachable` naming
|
|
489
|
+
* the store's origin for `network`.
|
|
433
490
|
*/
|
|
434
491
|
export class ArtifactFetchError extends ArtifactOperationError {
|
|
435
492
|
uri;
|
|
436
|
-
|
|
493
|
+
reason;
|
|
437
494
|
status;
|
|
438
|
-
constructor(message, uri,
|
|
439
|
-
super(message, {
|
|
495
|
+
constructor(message, uri, reason, status, options) {
|
|
496
|
+
super(message, {
|
|
497
|
+
cause: options?.cause,
|
|
498
|
+
problem: options?.problem ?? artifactFetchProblem(reason, uri, status),
|
|
499
|
+
});
|
|
440
500
|
this.name = "ArtifactFetchError";
|
|
441
501
|
this.uri = uri;
|
|
442
|
-
this.
|
|
502
|
+
this.reason = reason;
|
|
443
503
|
this.status = status;
|
|
444
504
|
}
|
|
445
505
|
}
|
|
446
|
-
/**
|
|
447
|
-
|
|
448
|
-
|
|
506
|
+
/**
|
|
507
|
+
* The problem of one reference's fetch failure that its raise site states none for, by its reason
|
|
508
|
+
* and the store's status: `input.reference_not_found` naming the reference for
|
|
509
|
+
* `invalid_storage_uri`, `forbidden` and `not_found`; `client.link_refused` for
|
|
510
|
+
* `unsupported_url`, `plain_http_refused` and `redirect_refused`; for `store_error` and
|
|
511
|
+
* `store_refused`, the object store failing or refusing a link the platform has just signed,
|
|
512
|
+
* `storageStatusProblem`'s reading of the store's status (`runtime.unavailable` for a `408`, a
|
|
513
|
+
* `429` or a `5xx` other than `501`, `runtime.unsupported` for a `501`, and
|
|
514
|
+
* `client.response_unreadable` naming any other status, a `401` or a `403` included), and
|
|
515
|
+
* `runtime.unavailable` when no status is known; `runtime.unavailable` for `timeout` and
|
|
516
|
+
* `network`; and `runtime.unexpected` otherwise, `unknown` and a reason this version does not know
|
|
517
|
+
* included.
|
|
518
|
+
*/
|
|
519
|
+
export function artifactFetchProblem(reason, uri, status) {
|
|
520
|
+
switch (reason) {
|
|
449
521
|
case "invalid_storage_uri":
|
|
450
522
|
case "forbidden":
|
|
451
|
-
case "unsupported_url":
|
|
452
523
|
case "not_found":
|
|
453
|
-
|
|
454
|
-
|
|
524
|
+
return buildProblem("input.reference_not_found", { input: uri });
|
|
525
|
+
case "unsupported_url":
|
|
526
|
+
return buildProblem("client.link_refused", { reason: "unsupported_scheme" });
|
|
455
527
|
case "plain_http_refused":
|
|
456
|
-
return
|
|
528
|
+
return buildProblem("client.link_refused", { reason: "plain_http" });
|
|
529
|
+
case "redirect_refused":
|
|
530
|
+
return buildProblem("client.link_refused", { reason: "redirect" });
|
|
531
|
+
case "store_error":
|
|
532
|
+
case "store_refused":
|
|
533
|
+
return status === undefined
|
|
534
|
+
? buildProblem("runtime.unavailable", {})
|
|
535
|
+
: storageStatusProblem(status);
|
|
457
536
|
case "timeout":
|
|
458
537
|
case "network":
|
|
459
|
-
return
|
|
460
|
-
case "store_error":
|
|
461
|
-
// The store's status reads as an API's would: refused for its timing (408, 429) or a fault
|
|
462
|
-
// that may pass (a 5xx) can succeed on a retry, while a 501 or a 4xx will not.
|
|
463
|
-
return makeVerdict("runtime", status !== undefined && fallbackVerdict(status, undefined, true).retryable);
|
|
538
|
+
return buildProblem("runtime.unavailable", {});
|
|
464
539
|
default:
|
|
465
|
-
|
|
466
|
-
return makeVerdict("runtime", false);
|
|
540
|
+
return buildProblem("runtime.unexpected", {});
|
|
467
541
|
}
|
|
468
542
|
}
|
|
469
543
|
/**
|
|
@@ -473,14 +547,19 @@ function artifactFetchVerdict(code, status) {
|
|
|
473
547
|
* carries the result as it stood: every item saved before the refusal, and
|
|
474
548
|
* the rest marked `aborted`. `status` is the route's status; the wrapped
|
|
475
549
|
* `ApiResponseError` is reachable via `cause`. (`verdict` is the download's result, not the
|
|
476
|
-
* error's own
|
|
477
|
-
*
|
|
550
|
+
* error's own problem.) Its problem is the server's own when the wrapped error answered a problem
|
|
551
|
+
* document; otherwise `auth.invalid_credential`, or `auth.missing_credential` when
|
|
552
|
+
* `options.credentialSent` says the request carried no credential, and the wrapped error's reading
|
|
553
|
+
* of the status when the caller does not say.
|
|
478
554
|
*/
|
|
479
555
|
export class ArtifactAuthenticationError extends ArtifactOperationError {
|
|
480
556
|
status;
|
|
481
557
|
verdict;
|
|
482
558
|
constructor(message, status, verdict, options) {
|
|
483
|
-
super(message, {
|
|
559
|
+
super(message, {
|
|
560
|
+
cause: options?.cause,
|
|
561
|
+
problem: credentialProblem(options?.cause, options?.credentialSent),
|
|
562
|
+
});
|
|
484
563
|
this.name = "ArtifactAuthenticationError";
|
|
485
564
|
this.status = status;
|
|
486
565
|
this.verdict = verdict;
|
|
@@ -495,77 +574,88 @@ export class ArtifactAuthenticationError extends ArtifactOperationError {
|
|
|
495
574
|
* `writeMethodTree` when a saved file could not be downloaded through its link, or its bytes are
|
|
496
575
|
* not the file the listing names.
|
|
497
576
|
*
|
|
498
|
-
* `operation` says which way the file was moving, `
|
|
577
|
+
* `operation` says which way the file was moving, `reason` why it failed, `sha256` which bytes,
|
|
499
578
|
* `path` the tree path when one is known, and `status` storage's HTTP status when it answered.
|
|
500
579
|
* Neither the message nor any member carries the link, whose query is a bearer credential, nor
|
|
501
580
|
* storage's error body, which can echo the signed request, and it keeps no `cause`, since the
|
|
502
581
|
* failure fetch throws can quote the URL.
|
|
503
582
|
*
|
|
504
|
-
* Its
|
|
505
|
-
* `
|
|
506
|
-
*
|
|
507
|
-
*
|
|
583
|
+
* Its problem follows `reason` (`methodFileTransferProblem`): `unreachable` and `storage_error`
|
|
584
|
+
* are `runtime.unavailable`, since the same transfer can succeed later; `refused` is
|
|
585
|
+
* `storageStatusProblem`'s reading of storage's status (`client.response_unreadable` naming it, or
|
|
586
|
+
* `runtime.unsupported` for a `501`), a download refused for an expired link succeeding once the
|
|
587
|
+
* method is read again, which serves fresh links; and `mismatch` is `client.response_unreadable`.
|
|
508
588
|
*/
|
|
509
589
|
export class MethodFileTransferError extends PipelexRequestError {
|
|
510
590
|
operation;
|
|
511
|
-
|
|
591
|
+
reason;
|
|
512
592
|
sha256;
|
|
513
593
|
path;
|
|
514
594
|
status;
|
|
515
595
|
constructor(message, details) {
|
|
516
|
-
super(message,
|
|
596
|
+
super(message, methodFileTransferProblem(details.reason, details.status));
|
|
517
597
|
this.name = "MethodFileTransferError";
|
|
518
598
|
this.operation = details.operation;
|
|
519
|
-
this.
|
|
599
|
+
this.reason = details.reason;
|
|
520
600
|
this.sha256 = details.sha256;
|
|
521
601
|
this.path = details.path;
|
|
522
602
|
this.status = details.status;
|
|
523
603
|
}
|
|
524
604
|
}
|
|
525
|
-
/** The
|
|
526
|
-
function
|
|
527
|
-
switch (
|
|
605
|
+
/** The problem of a failed transfer of a method's file, by its reason and storage's status. */
|
|
606
|
+
export function methodFileTransferProblem(reason, status) {
|
|
607
|
+
switch (reason) {
|
|
528
608
|
case "unreachable":
|
|
529
609
|
case "storage_error":
|
|
610
|
+
return buildProblem("runtime.unavailable", {});
|
|
611
|
+
case "refused":
|
|
612
|
+
return status === undefined
|
|
613
|
+
? buildProblem("runtime.unexpected", {})
|
|
614
|
+
: storageStatusProblem(status);
|
|
530
615
|
case "mismatch":
|
|
531
|
-
return
|
|
532
|
-
default:
|
|
533
|
-
// `refused`, and a code this version does not know.
|
|
534
|
-
return makeVerdict("runtime", false);
|
|
616
|
+
return responseUnreadableProblem(200);
|
|
535
617
|
}
|
|
536
618
|
}
|
|
619
|
+
// ── Transport ────────────────────────────────────────────────────────
|
|
537
620
|
/**
|
|
538
621
|
* Thrown when the Pipelex API host cannot be reached at all (DNS failure,
|
|
539
622
|
* connection refused, TLS handshake failure, request timeout). The HTTP
|
|
540
623
|
* exchange never produced a response — distinguish from `ApiResponseError`,
|
|
541
|
-
* which represents a
|
|
624
|
+
* which represents a response that did come back, a body that arrived and could not be decoded
|
|
625
|
+
* included.
|
|
542
626
|
*
|
|
543
|
-
* `
|
|
627
|
+
* `networkCode` is the underlying network error code when available
|
|
544
628
|
* (`ECONNREFUSED`, `ENOTFOUND`, `ETIMEDOUT`, `EAI_AGAIN`, `ABORT_TIMEOUT`).
|
|
545
629
|
*
|
|
546
|
-
* Its
|
|
547
|
-
*
|
|
548
|
-
* `
|
|
549
|
-
*
|
|
550
|
-
*
|
|
551
|
-
*
|
|
552
|
-
*
|
|
553
|
-
* request never left: the name lookup, the connection or the TLS handshake failed. A reset, a time
|
|
554
|
-
* limit or a body that could not be decoded proves nothing, so the run may have started and the
|
|
555
|
-
* error is not retryable.
|
|
630
|
+
* Its problem is the transport's: `client.timeout`, naming the time limit, for the SDK's own time
|
|
631
|
+
* limit (`ABORT_TIMEOUT`) when the client gives it as `options.timeoutMs`, and
|
|
632
|
+
* `client.unreachable`, naming the base URL, otherwise. Both have `retry` `yes`. On a request that
|
|
633
|
+
* may create a run (`options.mayStartRun`, which the client sets on `start` and `execute`), it is
|
|
634
|
+
* `client.outcome_unknown` instead, whose `retry` is `outcome_unknown`, unless `cause`, the failure
|
|
635
|
+
* fetch threw, proves the request never left: the name lookup, the connection or the TLS handshake
|
|
636
|
+
* failed. A reset or a time limit proves nothing, so the run may have started.
|
|
556
637
|
*/
|
|
557
638
|
export class ApiUnreachableError extends PipelexRequestError {
|
|
558
639
|
apiUrl;
|
|
559
|
-
|
|
560
|
-
constructor(message, apiUrl,
|
|
561
|
-
super(message,
|
|
562
|
-
...options,
|
|
563
|
-
runMayHaveStarted: options?.mayStartRun === true && transportMayHaveStartedRun(options.cause),
|
|
564
|
-
});
|
|
640
|
+
networkCode;
|
|
641
|
+
constructor(message, apiUrl, networkCode, options) {
|
|
642
|
+
super(message, transportProblem(apiUrl, networkCode, options), causeOption(options?.cause));
|
|
565
643
|
this.name = "ApiUnreachableError";
|
|
566
644
|
this.apiUrl = apiUrl;
|
|
567
|
-
this.
|
|
645
|
+
this.networkCode = networkCode;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
/** The SDK's own time limit ran out while the request was sent or answered. */
|
|
649
|
+
export const ABORT_TIMEOUT = "ABORT_TIMEOUT";
|
|
650
|
+
/** The problem of a request that got no answer, agreeing with whether it may have left. */
|
|
651
|
+
function transportProblem(apiUrl, networkCode, options) {
|
|
652
|
+
if (options?.mayStartRun === true && requestMayHaveLeft(options.cause)) {
|
|
653
|
+
return outcomeUnknownProblem();
|
|
654
|
+
}
|
|
655
|
+
if (networkCode === ABORT_TIMEOUT && options?.timeoutMs !== undefined) {
|
|
656
|
+
return buildProblem("client.timeout", { seconds: options.timeoutMs / 1000 });
|
|
568
657
|
}
|
|
658
|
+
return buildProblem("client.unreachable", { base_url: apiUrl });
|
|
569
659
|
}
|
|
570
660
|
/**
|
|
571
661
|
* Thrown when the blocking `execute` (`POST /v1/execute`) is killed by the
|
|
@@ -573,9 +663,9 @@ export class ApiUnreachableError extends PipelexRequestError {
|
|
|
573
663
|
* run methods longer than 30s behind the hosted gateway — use the durable run
|
|
574
664
|
* lifecycle (start + poll) instead.
|
|
575
665
|
*
|
|
576
|
-
* Its
|
|
577
|
-
*
|
|
578
|
-
*
|
|
666
|
+
* Its problem is `client.outcome_unknown`: the gateway gave up on the request, and the run it
|
|
667
|
+
* carried may go on without its id reaching the caller, so it is looked up in the run history
|
|
668
|
+
* rather than sent again, and the next one is started and polled.
|
|
579
669
|
*/
|
|
580
670
|
export class PipelineExecuteTimeoutError extends PipelexRequestError {
|
|
581
671
|
elapsedMs;
|
|
@@ -583,62 +673,49 @@ export class PipelineExecuteTimeoutError extends PipelexRequestError {
|
|
|
583
673
|
const seconds = Math.round(elapsedMs / 1000);
|
|
584
674
|
super(`The Pipelex Hosted API times out synchronous requests after ~30s — this run took ${seconds}s. ` +
|
|
585
675
|
"The blocking execute path can't run methods longer than 30s behind the gateway. " +
|
|
586
|
-
"Start the run and poll for its result instead: `start()` then `waitForResult(runId)`.",
|
|
676
|
+
"Start the run and poll for its result instead: `start()` then `waitForResult(runId)`.", outcomeUnknownProblem(), causeOption(options?.cause));
|
|
587
677
|
this.name = "PipelineExecuteTimeoutError";
|
|
588
678
|
this.elapsedMs = elapsedMs;
|
|
589
679
|
}
|
|
590
680
|
}
|
|
681
|
+
// ── The run lifecycle ────────────────────────────────────────────────
|
|
682
|
+
/**
|
|
683
|
+
* The problem of a run that ended without success, `run.failed`, naming the run and its terminal
|
|
684
|
+
* status, with the run's own problem nested as `run_error` when there is one.
|
|
685
|
+
*/
|
|
686
|
+
export function runFailedProblem(runId, status, runError) {
|
|
687
|
+
return buildProblem("run.failed", { pipeline_run_id: runId, run_status: status }, { pipeline_run_id: runId, extra: { run_error: runError ?? undefined } });
|
|
688
|
+
}
|
|
591
689
|
/**
|
|
592
690
|
* Thrown when a run reaches a terminal state that is not `COMPLETED` (`FAILED`, `CANCELLED`,
|
|
593
691
|
* `TERMINATED`, `TIMED_OUT`) — by `waitForResult`, `startAndWaitForResult` and the artifact
|
|
594
692
|
* download when the platform answers the results read with HTTP 409. (`getRunResult` returns
|
|
595
693
|
* the same facts as its `failed` arm instead of throwing.)
|
|
596
694
|
*
|
|
597
|
-
* - `
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
* `
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
606
|
-
* `An internal error occurred.` otherwise, `user_action` comes only with caller-facing copy,
|
|
607
|
-
* `provider_metadata` keeps only `status_code` and `retry_after_seconds`, and `model`,
|
|
608
|
-
* `provider` and `migration` are absent; deciding what a person sees is still the consumer's.
|
|
609
|
-
* A run the platform ended or settled itself carries the caller-facing report the platform
|
|
610
|
-
* wrote, in the same shape. A cancelled, terminated or timed-out run carries a report when a
|
|
611
|
-
* completion callback carried one, and a timed-out run otherwise the platform's timeout report.
|
|
612
|
-
* `null` when the run ended with no stored report (one whose start failed, or one cancelled,
|
|
613
|
-
* terminated or given its status by hand with no completion callback carrying a report).
|
|
614
|
-
* - The error's own `message` is the problem's `detail`, which names the status and then the
|
|
615
|
-
* report's message (`Run finished with status FAILED: <message>`), so printing the error
|
|
616
|
-
* already tells the reason.
|
|
617
|
-
* - `runId` locates the run, for a status read or a support request.
|
|
618
|
-
*
|
|
619
|
-
* Its verdict comes from the report. `errorDomain` is the report's `error_domain` when it is one
|
|
620
|
-
* of the three domains, and `runtime` otherwise, including a run with no report. `retryable` is
|
|
621
|
-
* true only when the report's `retryable` is `true`: a report that says nothing, and a run with
|
|
622
|
-
* none, read as not retryable, since starting the run again spends credit and nothing says it
|
|
623
|
-
* would succeed. The report keeps its own `retryable` as written, so a consumer that words the
|
|
624
|
-
* unknown differently still can.
|
|
695
|
+
* - `problem` is `run.failed`, the results read's own problem document as the platform answered
|
|
696
|
+
* it, or the one the SDK builds for a failed run it learned of otherwise. Its `retry` is `no`:
|
|
697
|
+
* the run is over, and starting it again spends credit.
|
|
698
|
+
* - `runError` is the run's own problem, which `run.failed` nests as `run_error`: why the run did
|
|
699
|
+
* not succeed, under its own code (`run.cancelled`, `run.timed_out`, a model's or a method's
|
|
700
|
+
* code), with its own `domain`, `retry` and `next_step`. Branch on it to tell a run worth
|
|
701
|
+
* starting again from one whose method or inputs must change. `undefined` when the run ended
|
|
702
|
+
* with no problem stored.
|
|
703
|
+
* - `status` is the run's terminal status, `runId` the run.
|
|
625
704
|
*/
|
|
626
705
|
export class RunFailedError extends PipelexRequestError {
|
|
627
706
|
runId;
|
|
628
707
|
status;
|
|
629
|
-
error;
|
|
630
708
|
constructor(message, runId, status, options) {
|
|
631
|
-
super(message,
|
|
709
|
+
super(message, options?.problem ?? runFailedProblem(runId, status), causeOption(options?.cause));
|
|
632
710
|
this.name = "RunFailedError";
|
|
633
711
|
this.runId = runId;
|
|
634
712
|
this.status = status;
|
|
635
|
-
this.error = options?.error ?? null;
|
|
636
713
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
714
|
+
/** The run's own problem, nested as `run_error`, when the run ended with one. */
|
|
715
|
+
get runError() {
|
|
716
|
+
const nested = this.problem.run_error;
|
|
717
|
+
return isProblem(nested) ? nested : undefined;
|
|
718
|
+
}
|
|
642
719
|
}
|
|
643
720
|
/**
|
|
644
721
|
* Thrown when a completed run cannot deliver its main stuff.
|
|
@@ -650,28 +727,34 @@ function runFailureVerdict(report) {
|
|
|
650
727
|
* memory. `runId` locates the run. (An empty-but-present main stuff — `{ items: [] }`, `{ text:
|
|
651
728
|
* "" }` — is a valid output and does NOT throw; only a genuinely absent one does.)
|
|
652
729
|
*
|
|
653
|
-
* Its
|
|
730
|
+
* Its problem is `client.response_unreadable`, naming the run: the API broke its own contract on a
|
|
731
|
+
* completed run.
|
|
654
732
|
*/
|
|
655
733
|
export class MissingMainStuffError extends PipelexRequestError {
|
|
656
734
|
runId;
|
|
657
735
|
constructor(message, runId) {
|
|
658
|
-
super(message,
|
|
736
|
+
super(message, responseUnreadableProblem(200, runId));
|
|
659
737
|
this.name = "MissingMainStuffError";
|
|
660
738
|
this.runId = runId;
|
|
661
739
|
}
|
|
662
740
|
}
|
|
741
|
+
/** `client.follow_failed`: the client stopped following a run that may still be going. */
|
|
742
|
+
function followFailedProblem(runId) {
|
|
743
|
+
return buildProblem("client.follow_failed", { pipeline_run_id: runId }, { pipeline_run_id: runId });
|
|
744
|
+
}
|
|
663
745
|
/**
|
|
664
746
|
* Thrown when `waitForResult` exceeds its `timeoutMs` before the run reaches a
|
|
665
747
|
* terminal state. The run is NOT cancelled — it keeps executing server-side and
|
|
666
748
|
* can be resumed later by `runId` (the poll loop just stopped waiting).
|
|
667
749
|
*
|
|
668
|
-
* Its
|
|
750
|
+
* Its problem is `client.follow_failed`, naming the run: its `retry` is `outcome_unknown` and its
|
|
751
|
+
* step `read_run`, since the run goes on and is read by its id, never started again.
|
|
669
752
|
*/
|
|
670
753
|
export class RunTimeoutError extends PipelexRequestError {
|
|
671
754
|
runId;
|
|
672
755
|
timeoutMs;
|
|
673
756
|
constructor(message, runId, timeoutMs) {
|
|
674
|
-
super(message,
|
|
757
|
+
super(message, followFailedProblem(runId));
|
|
675
758
|
this.name = "RunTimeoutError";
|
|
676
759
|
this.runId = runId;
|
|
677
760
|
this.timeoutMs = timeoutMs;
|
|
@@ -686,14 +769,15 @@ export class RunTimeoutError extends PipelexRequestError {
|
|
|
686
769
|
* server-side — resume by `runId` (`getRunResult` / `waitForResult` on a
|
|
687
770
|
* hosted deployment, or the `location` status resource when provided).
|
|
688
771
|
*
|
|
689
|
-
* Its
|
|
772
|
+
* Its problem is `client.follow_failed`, naming the run: its `retry` is `outcome_unknown`, since
|
|
773
|
+
* the run exists and executing again would pay for a second one, and its step `read_run`.
|
|
690
774
|
*/
|
|
691
775
|
export class RunStillRunningError extends PipelexRequestError {
|
|
692
776
|
runId;
|
|
693
777
|
retryAfterSeconds;
|
|
694
778
|
location;
|
|
695
779
|
constructor(message, runId, retryAfterSeconds = null, location = null, options) {
|
|
696
|
-
super(message,
|
|
780
|
+
super(message, followFailedProblem(runId), causeOption(options?.cause));
|
|
697
781
|
this.name = "RunStillRunningError";
|
|
698
782
|
this.runId = runId;
|
|
699
783
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
@@ -707,16 +791,17 @@ export class RunStillRunningError extends PipelexRequestError {
|
|
|
707
791
|
* Run polling is a hosted-API extension, not part of the MTHDS Protocol: the
|
|
708
792
|
* open-source `pipelex-api` runner executes methods but has no run store, so
|
|
709
793
|
* it 404s those routes; only a deployment that includes the platform block
|
|
710
|
-
* (the Pipelex Hosted API) serves status/results.
|
|
711
|
-
*
|
|
794
|
+
* (the Pipelex Hosted API) serves status/results. A route the deployment lacks answers a `404`
|
|
795
|
+
* whose body is no problem document; a run the platform does not know answers a `404` problem
|
|
796
|
+
* document, `run.not_found`, which is an `ApiResponseError`.
|
|
712
797
|
*
|
|
713
|
-
* Its
|
|
714
|
-
*
|
|
798
|
+
* Its problem is `runtime.unsupported`, naming the durable run lifecycle as the capability the
|
|
799
|
+
* deployment lacks: the base URL points at a bare runner without it.
|
|
715
800
|
*/
|
|
716
801
|
export class RunLifecycleUnavailableError extends PipelexRequestError {
|
|
717
802
|
apiUrl;
|
|
718
803
|
constructor(message, apiUrl, options) {
|
|
719
|
-
super(message,
|
|
804
|
+
super(message, buildProblem("runtime.unsupported", { capability: "the durable run lifecycle" }), causeOption(options?.cause));
|
|
720
805
|
this.name = "RunLifecycleUnavailableError";
|
|
721
806
|
this.apiUrl = apiUrl;
|
|
722
807
|
}
|
|
@@ -728,262 +813,69 @@ export class RunLifecycleUnavailableError extends PipelexRequestError {
|
|
|
728
813
|
* The ceiling, `pageLimit` pages, sits far beyond any real catalog, run history or file list, so reaching it
|
|
729
814
|
* is a server-side fault — an endpoint minting a fresh cursor forever — not a coverage limit the
|
|
730
815
|
* caller can raise. Throwing beats returning, because a silently truncated list is exactly the bug
|
|
731
|
-
* paging was introduced to remove. Its
|
|
816
|
+
* paging was introduced to remove. Its problem is `client.response_unreadable`.
|
|
732
817
|
*/
|
|
733
818
|
export class PagingNotTerminatingError extends PipelexRequestError {
|
|
734
819
|
pageLimit;
|
|
735
820
|
constructor(message, pageLimit) {
|
|
736
|
-
super(message,
|
|
821
|
+
super(message, responseUnreadableProblem(200));
|
|
737
822
|
this.name = "PagingNotTerminatingError";
|
|
738
823
|
this.pageLimit = pageLimit;
|
|
739
824
|
}
|
|
740
825
|
}
|
|
826
|
+
/** The response header carrying the request's correlation id. */
|
|
827
|
+
const REQUEST_ID_HEADER = "x-request-id";
|
|
828
|
+
/** A header's value, as fetch gives the headers or as a plain map of names. */
|
|
829
|
+
function headerValue(headers, name) {
|
|
830
|
+
if (headers === undefined)
|
|
831
|
+
return undefined;
|
|
832
|
+
if (typeof headers.get === "function") {
|
|
833
|
+
const value = headers.get(name);
|
|
834
|
+
return value === null || value === "" ? undefined : value;
|
|
835
|
+
}
|
|
836
|
+
const map = headers;
|
|
837
|
+
const found = Object.keys(map).find((key) => key.toLowerCase() === name);
|
|
838
|
+
const value = found === undefined ? undefined : map[found];
|
|
839
|
+
return value === undefined || value === "" ? undefined : value;
|
|
840
|
+
}
|
|
741
841
|
/**
|
|
742
842
|
* A response that DID come back from the API and that the SDK cannot hand back as a result: a
|
|
743
|
-
* non-2xx refusal,
|
|
744
|
-
* a body that is not JSON, JSON that is not the object the route answers, or a stored
|
|
745
|
-
* whose `python` field is not the serialized file list.
|
|
746
|
-
*
|
|
747
|
-
*
|
|
748
|
-
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
751
|
-
*
|
|
752
|
-
*
|
|
753
|
-
*
|
|
754
|
-
*
|
|
755
|
-
*
|
|
756
|
-
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
* `config` (a configuration change), `runtime` (nobody beforehand) — and `retryable` whether
|
|
760
|
-
* asking again can succeed. Each is the document's member when the server sent a valid one,
|
|
761
|
-
* and otherwise the SDK's fallback, read from the status, the platform `code` and whether the
|
|
762
|
-
* body names what it refused. A runner often sends `error_domain` without `retryable`, and
|
|
763
|
-
* then the domain it sent decides `retryable`: `input` and `config` are not retryable, as a
|
|
764
|
-
* stored report that says nothing is not, since the caller or the environment must change,
|
|
765
|
-
* while `runtime` takes the fallback's. Nothing on the error says which a value came from:
|
|
766
|
-
* `problemDocument` keeps what the server sent.
|
|
767
|
-
* - **A run that may have started.** On a `start` or an `execute`, `runMayHaveStarted` says
|
|
768
|
-
* whether the answer leaves a run possibly started without its id reaching the caller: any
|
|
769
|
-
* `2xx` the SDK could not read, a `408`, and every `5xx` but a `501` and the platform's `503`
|
|
770
|
-
* coded `pipeline_start_unavailable`, which says the runner was never reached. Then
|
|
771
|
-
* `retryable` is false, whatever the server sent: asking again could start a second run. A
|
|
772
|
-
* runner's own failure of a blocking execute reads the same, since the SDK cannot tell a run
|
|
773
|
-
* that ended from one that goes on, and its `retryable` stays readable on `problemDocument`.
|
|
774
|
-
* On every other route it is false.
|
|
775
|
-
* - **The branch fields.** `errorDomain`, and `type`, the stable URI naming the error class.
|
|
776
|
-
* Branch on these, never on the HTTP status or on the wording of a message; they are the
|
|
777
|
-
* fields that mean the same on both surfaces.
|
|
778
|
-
* - **The native codes.** `code` is the platform's own closed code (`conflict`, `not_found`,
|
|
779
|
-
* `pipelex_api_key_limit_reached`, …; `MethodErrorCode` names the method ones and
|
|
780
|
-
* `IdempotencyErrorCode` the idempotency key's) and `errorType` the runner's open exception
|
|
781
|
-
* class name. Each is finer than `errorDomain` and
|
|
782
|
-
* specific to the surface that emits it, and a client of that surface may branch on it: the
|
|
783
|
-
* platform's `type` is derived from its `code` one to one
|
|
784
|
-
* (`https://pipelex.com/errors/<code>`), so the two read the same answer. A problem the
|
|
785
|
-
* platform relays from the runner carries the runner's `type` and `errorType` and no `code`.
|
|
786
|
-
* - **For a person.** `title` is the stable label of the error class, `serverMessage` the
|
|
787
|
-
* per-occurrence `detail`, `userAction` the advised next step, and `errorCategory`, `model`,
|
|
788
|
-
* `provider` and `providerMetadata` describe an inference failure.
|
|
789
|
-
* - **For support.** `requestId` correlates the response with the server's logs; it is read
|
|
790
|
-
* from the body, or from the `X-Request-ID` response header when the body has none.
|
|
791
|
-
* `instance` is the occurrence's URN or request path.
|
|
792
|
-
* - **Per-item failures.** `errors` is the platform's field-level list (`field`, `code`,
|
|
793
|
-
* `detail`), and `validationErrors` the structured diagnostics of a bundle that failed
|
|
794
|
-
* validation.
|
|
795
|
-
* - **The standard's reading.** `problem` is the same answer read as the MTHDS standard reads
|
|
796
|
-
* it, the problem `mthds`'s own `ApiResponseError` carries: the body itself when it is a core
|
|
797
|
-
* problem document, and otherwise the problem its status reads as (`readAnswer` in
|
|
798
|
-
* `mthds/protocol`), except on a request that may create a run, where `runMayHaveStarted`
|
|
799
|
-
* decides: `client.outcome_unknown` for every answer that leaves a run possibly started, a core
|
|
800
|
-
* problem document's body included. Branch on `problem.code` to handle this SDK's errors and the
|
|
801
|
-
* standard client's alike. The hosted plane's problem documents predate the core registry, so
|
|
802
|
-
* for them `problem` says little; the members above are read from the hosted document as they
|
|
803
|
-
* always were, and those the standard names its own way do not collide with it: `code` is the
|
|
804
|
-
* platform's code beside the registry's `problem.code`, and `errors` the platform's field
|
|
805
|
-
* errors beside the problem items of `problem.errors`.
|
|
806
|
-
*
|
|
807
|
-
* `problemDocument` is the decoded document whole, so a member this SDK does not name stays
|
|
808
|
-
* reachable without re-parsing `responseBody`, which is the raw text; it is `undefined` when
|
|
809
|
-
* the body was not a JSON object.
|
|
810
|
-
*
|
|
811
|
-
* The error is a `PipelineRequestError`, as every error of this SDK is, rather than the
|
|
812
|
-
* standard's `ProblemError`: a `ProblemError`'s `code` is the registry's and its message the
|
|
813
|
-
* problem's sentence, while this error's `code` is the platform's and its message names the
|
|
814
|
-
* route and the hosted document's `detail`, so it carries the standard's reading as `problem`
|
|
815
|
-
* instead.
|
|
843
|
+
* non-2xx refusal, a body that arrived and could not be decoded, or a 2xx answer the SDK could not
|
|
844
|
+
* read — a body that is not JSON, JSON that is not the object the route answers, or a stored
|
|
845
|
+
* method whose `python` field is not the serialized file list.
|
|
846
|
+
*
|
|
847
|
+
* Its `problem` is the answer read as a problem (`readHostedAnswer`): the body itself when it is a
|
|
848
|
+
* problem document, as the hosted API answers every refusal, its `pipelex.*` code and any member
|
|
849
|
+
* the standard does not name kept; and otherwise the reading of the edge's bare answers by its
|
|
850
|
+
* status, `client.outcome_unknown` among them for a start or an execute the gateway cut off. Every
|
|
851
|
+
* getter of `ProblemError` reads it: branch on `code`, never on the HTTP status or the wording of
|
|
852
|
+
* a message. `retryAfter` is the problem's `retry_after`, which the `Retry-After` header fills when
|
|
853
|
+
* the problem invites a retry and gives none, and `requestId` the problem's `request_id`, else the
|
|
854
|
+
* answer's `X-Request-ID` header, the id to hand to support.
|
|
855
|
+
*
|
|
856
|
+
* `status` is the answer's HTTP status whatever the problem's own `status` says, `statusText` its
|
|
857
|
+
* reason phrase and `responseBody` its raw text, so a member this SDK does not name stays
|
|
858
|
+
* reachable without re-reading the body.
|
|
816
859
|
*/
|
|
817
860
|
export class ApiResponseError extends PipelexRequestError {
|
|
818
861
|
apiUrl;
|
|
819
862
|
status;
|
|
820
863
|
statusText;
|
|
821
864
|
responseBody;
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
* `pipelex_api_key_limit_reached`, …), one-to-one with `type`; `MethodErrorCode` names the
|
|
827
|
-
* ones about a stored method and its versions, and `IdempotencyErrorCode` the one about a
|
|
828
|
-
* request's idempotency key. Finer than `errorDomain` and specific to the
|
|
829
|
-
* platform: a runner's problem carries `errorType` instead. `undefined` for a body that
|
|
830
|
-
* carries no `code`.
|
|
831
|
-
*/
|
|
832
|
-
code;
|
|
833
|
-
/**
|
|
834
|
-
* RFC 9457 `type`: the stable URI naming the error class. With `errorDomain`, the field a
|
|
835
|
-
* machine consumer branches on — the same class carries the same URI on every occurrence.
|
|
836
|
-
*/
|
|
837
|
-
type;
|
|
838
|
-
/** RFC 9457 `title`: the short human label of the error class. */
|
|
839
|
-
title;
|
|
840
|
-
/** RFC 9457 `instance`: the occurrence — the request path, or a request URN. */
|
|
841
|
-
instance;
|
|
842
|
-
/**
|
|
843
|
-
* The request's correlation id — the body's `request_id`, or the `X-Request-ID` response
|
|
844
|
-
* header when the body carries none. The id to hand to support: it finds the server's log
|
|
845
|
-
* lines for this request.
|
|
846
|
-
*/
|
|
847
|
-
requestId;
|
|
848
|
-
/**
|
|
849
|
-
* The body's `error_category`: the finer classification of an inference failure — known
|
|
850
|
-
* values `transient`, `configuration`, `content`, `capacity`, `ambiguous`, `unknown`.
|
|
851
|
-
*/
|
|
852
|
-
errorCategory;
|
|
853
|
-
/** The body's `user_action`: what the caller should do next, when the server can say. */
|
|
854
|
-
userAction;
|
|
855
|
-
/** The body's `model`: the model an inference failure used. */
|
|
856
|
-
model;
|
|
857
|
-
/** The body's `provider`: the provider an inference failure reached. */
|
|
858
|
-
provider;
|
|
859
|
-
/**
|
|
860
|
-
* The body's `provider_metadata`: what the provider's SDK said. On the hosted API it comes
|
|
861
|
-
* from the runner's problem document, which the hosted runner renders under its STRICT
|
|
862
|
-
* disclosure, so it keeps only `status_code` and `retry_after_seconds`; a runner that
|
|
863
|
-
* discloses errors VERBOSE, as a self-hosted one does when `ERROR_DISCLOSURE` is unset, sends
|
|
864
|
-
* it whole, the provider's raw text included.
|
|
865
|
-
*/
|
|
866
|
-
providerMetadata;
|
|
867
|
-
/** The body's `migration`: a pending configuration migration that explains the failure. */
|
|
868
|
-
migration;
|
|
869
|
-
/**
|
|
870
|
-
* The platform's field-level `errors[]` — one item per offending request field. A refused
|
|
871
|
-
* branding write names the logo slot or the colour key each entry is about in its `field`,
|
|
872
|
-
* with a `LogoRefusalCode` for a logo the platform measured.
|
|
873
|
-
*/
|
|
874
|
-
errors;
|
|
875
|
-
/**
|
|
876
|
-
* The decoded problem document whole — every member, named or not, such as a failed run's
|
|
877
|
-
* `run_status` and `error` or a member a newer server adds. `undefined` when the body was not
|
|
878
|
-
* a JSON object.
|
|
879
|
-
*/
|
|
880
|
-
problemDocument;
|
|
881
|
-
/**
|
|
882
|
-
* Structured per-error diagnostics on a problem body that carries a top-level
|
|
883
|
-
* `validation_errors[]` — the 422 a **run route** (`execute`, `start`) answers when the
|
|
884
|
-
* runner refuses the method for its validation errors.
|
|
885
|
-
*
|
|
886
|
-
* Everywhere else an invalid bundle is a produced verdict, not an `ApiResponseError`:
|
|
887
|
-
* `POST /v1/validate` answers it with a **200** `PipelexInvalidReport`, and the crate
|
|
888
|
-
* routes with a **200** `CrateInvalidReport`, whose `validation_errors[]` the caller
|
|
889
|
-
* reads off the returned value. This field is `undefined` for any error that carries no
|
|
890
|
-
* per-error list (auth, transport, a request-shape 422, the 422 `pipe-io` answers for a
|
|
891
|
-
* pipe selection it refuses). A consumer must NOT assume a given `error_type` implies a
|
|
892
|
-
* populated list — fall back to `serverMessage` when this is empty.
|
|
893
|
-
*/
|
|
894
|
-
validationErrors;
|
|
895
|
-
/**
|
|
896
|
-
* The answer read as the MTHDS standard reads it, the problem `mthds`'s own client carries,
|
|
897
|
-
* agreeing with `runMayHaveStarted`: `client.outcome_unknown` where a run may have started,
|
|
898
|
-
* whatever the body, and where none started the body when it is a core problem document,
|
|
899
|
-
* otherwise the reading of an answer to a request that creates no run.
|
|
900
|
-
* Built from the constructor's arguments, so an error a consumer builds by hand carries the
|
|
901
|
-
* reading the client would give it.
|
|
902
|
-
*/
|
|
903
|
-
problem;
|
|
904
|
-
constructor(message, apiUrl, status, statusText, responseBody, errorType, serverMessage, validationErrors, code, options) {
|
|
905
|
-
super(message, responseVerdict(status, errorType, code, options?.details), {
|
|
906
|
-
...(options?.cause === undefined ? {} : { cause: options.cause }),
|
|
907
|
-
runMayHaveStarted: options?.mayStartRun === true && answerMayHaveStartedRun(status, code),
|
|
908
|
-
});
|
|
865
|
+
requestIdHeader;
|
|
866
|
+
constructor(message, apiUrl, status, statusText, responseBody, options) {
|
|
867
|
+
super(message, options?.problem ??
|
|
868
|
+
readHostedAnswer({ status, headers: options?.headers ?? {}, body: responseBody }, { createsRun: options?.mayStartRun === true }).problem, causeOption(options?.cause));
|
|
909
869
|
this.name = "ApiResponseError";
|
|
910
870
|
this.apiUrl = apiUrl;
|
|
911
871
|
this.status = status;
|
|
912
872
|
this.statusText = statusText;
|
|
913
873
|
this.responseBody = responseBody;
|
|
914
|
-
this.
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
this.title = details?.title;
|
|
921
|
-
this.instance = details?.instance;
|
|
922
|
-
this.requestId = details?.requestId;
|
|
923
|
-
this.errorCategory = details?.errorCategory;
|
|
924
|
-
this.userAction = details?.userAction;
|
|
925
|
-
this.model = details?.model;
|
|
926
|
-
this.provider = details?.provider;
|
|
927
|
-
this.providerMetadata = details?.providerMetadata;
|
|
928
|
-
this.migration = details?.migration;
|
|
929
|
-
this.errors = details?.errors;
|
|
930
|
-
this.problemDocument = options?.problemDocument;
|
|
931
|
-
this.problem = standardReading(status, responseBody, options?.headers ?? {}, options?.mayStartRun === true, code);
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
/**
|
|
935
|
-
* An answer read as the MTHDS standard reads it, as `mthds`'s own client does, agreeing with the
|
|
936
|
-
* error's `runMayHaveStarted`: on a request that may create a run, the SDK's own reading of
|
|
937
|
-
* whether a run may have started (`answerMayHaveStartedRun`) decides, as an
|
|
938
|
-
* `ApiUnreachableError`'s does for a request that got no answer, so that `problem` never says a
|
|
939
|
-
* run may exist where `runMayHaveStarted` says none started, nor the reverse.
|
|
940
|
-
*
|
|
941
|
-
* - Where a run may have started, the answer reads as `client.outcome_unknown`, naming the run
|
|
942
|
-
* when the body names one (the platform's `bad_gateway` does when it lost a start's
|
|
943
|
-
* acknowledgement), whatever the body: a `408`, which a proxy may answer for a request it
|
|
944
|
-
* forwarded, every `5xx` but a `501` and a `503` carrying the platform's code
|
|
945
|
-
* `pipeline_start_unavailable`, and a `2xx` the SDK could not read, since the server accepted
|
|
946
|
-
* the request. A core problem document in such a body is not kept, since one inviting a retry
|
|
947
|
-
* would contradict `runMayHaveStarted`.
|
|
948
|
-
* - Where none started, which is every answer to a request that creates no run, a status outside
|
|
949
|
-
* `2xx` whose body is a core problem document reads as that document, and any other as the
|
|
950
|
-
* answer to a request that creates none: the code its status names (`runtime.unavailable` for
|
|
951
|
-
* that `503`), else `client.response_unreadable` naming the status (that `501`). A `2xx` the SDK
|
|
952
|
-
* could not read is `client.response_unreadable`.
|
|
953
|
-
*/
|
|
954
|
-
function standardReading(status, body, headers, mayStartRun, code) {
|
|
955
|
-
const runMayHaveStarted = mayStartRun && answerMayHaveStartedRun(status, code);
|
|
956
|
-
if (!runMayHaveStarted) {
|
|
957
|
-
if (status >= 200 && status < 300)
|
|
958
|
-
return unreadableAnswer(status);
|
|
959
|
-
return readAnswer({ status, headers, body }, { createsRun: false }).problem;
|
|
960
|
-
}
|
|
961
|
-
const reading = parseJsonBody(body);
|
|
962
|
-
const runId = reading.ok && isPlainObject(reading.value) ? reading.value.pipeline_run_id : undefined;
|
|
963
|
-
if (typeof runId !== "string" || runId === "") {
|
|
964
|
-
return buildProblem("client.outcome_unknown", {});
|
|
965
|
-
}
|
|
966
|
-
return buildProblem("client.outcome_unknown", { pipeline_run_id: runId }, { pipeline_run_id: runId });
|
|
967
|
-
}
|
|
968
|
-
/**
|
|
969
|
-
* The verdict of a refused request: each member the document's own when it is valid, and the
|
|
970
|
-
* fallback's otherwise, except that a sent `input` or `config` domain with no sent `retryable` is
|
|
971
|
-
* not retryable. A `404` is named when the body carries a platform `code` or a runner
|
|
972
|
-
* `error_type`, read from the constructor's arguments, so an `ApiResponseError` a consumer builds
|
|
973
|
-
* by hand gets the verdict the client would give it.
|
|
974
|
-
*/
|
|
975
|
-
function responseVerdict(status, errorType, code, problem) {
|
|
976
|
-
const fallback = fallbackVerdict(status, code, code !== undefined || errorType !== undefined);
|
|
977
|
-
const sentDomain = problem?.errorDomain;
|
|
978
|
-
const sentRetryable = problem?.retryable;
|
|
979
|
-
if (!isErrorDomain(sentDomain)) {
|
|
980
|
-
return makeVerdict(fallback.errorDomain, typeof sentRetryable === "boolean" ? sentRetryable : fallback.retryable);
|
|
981
|
-
}
|
|
982
|
-
if (typeof sentRetryable === "boolean")
|
|
983
|
-
return makeVerdict(sentDomain, sentRetryable);
|
|
984
|
-
// The server said who fixes the failure and nothing of a retry. When the caller or the
|
|
985
|
-
// environment must change, asking again unchanged meets the same answer, whatever the status
|
|
986
|
-
// would suggest; only a `runtime` fault keeps the status's reading.
|
|
987
|
-
return makeVerdict(sentDomain, sentDomain === "runtime" ? fallback.retryable : false);
|
|
874
|
+
this.requestIdHeader = headerValue(options?.headers, REQUEST_ID_HEADER);
|
|
875
|
+
}
|
|
876
|
+
/** The request's id: the problem's `request_id`, else the answer's `X-Request-ID` header. */
|
|
877
|
+
get requestId() {
|
|
878
|
+
return super.requestId ?? this.requestIdHeader;
|
|
879
|
+
}
|
|
988
880
|
}
|
|
989
881
|
//# sourceMappingURL=errors.js.map
|