@telorun/sdk 0.48.0 → 0.49.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/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/invoke-error.d.ts +3 -1
- package/dist/invoke-error.d.ts.map +1 -1
- package/dist/invoke-error.js +14 -1
- package/dist/network-fetch.d.ts +71 -0
- package/dist/network-fetch.d.ts.map +1 -0
- package/dist/network-fetch.js +140 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/invoke-error.ts +14 -1
- package/src/network-fetch.ts +175 -0
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./resource-context.js";
|
|
|
17
17
|
export * from "./resource-instance.js";
|
|
18
18
|
export * from "./resource-manifest.js";
|
|
19
19
|
export * from "./invoke-error.js";
|
|
20
|
+
export * from "./network-fetch.js";
|
|
20
21
|
export * from "./runtime-error.js";
|
|
21
22
|
export * from "./runtime-event.js";
|
|
22
23
|
export * from "./runtime-resource.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,UAAU,CAAC;AACzB,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,UAAU,CAAC;AACzB,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,uBAAuB,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,yBAAyB,CAAC;AACxC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,uBAAuB,CAAC;AACtC,cAAc,aAAa,CAAC;AAC5B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./resource-context.js";
|
|
|
17
17
|
export * from "./resource-instance.js";
|
|
18
18
|
export * from "./resource-manifest.js";
|
|
19
19
|
export * from "./invoke-error.js";
|
|
20
|
+
export * from "./network-fetch.js";
|
|
20
21
|
export * from "./runtime-error.js";
|
|
21
22
|
export * from "./runtime-event.js";
|
|
22
23
|
export * from "./runtime-resource.js";
|
package/dist/invoke-error.d.ts
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
export declare class InvokeError extends Error {
|
|
9
9
|
readonly code: string;
|
|
10
10
|
readonly data?: unknown;
|
|
11
|
-
constructor(code: string, message: string, data?: unknown
|
|
11
|
+
constructor(code: string, message: string, data?: unknown, options?: {
|
|
12
|
+
cause?: unknown;
|
|
13
|
+
});
|
|
12
14
|
}
|
|
13
15
|
export declare function isInvokeError(err: unknown): err is InvokeError;
|
|
14
16
|
//# sourceMappingURL=invoke-error.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invoke-error.d.ts","sourceRoot":"","sources":["../src/invoke-error.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;gBAEZ,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO;
|
|
1
|
+
{"version":3,"file":"invoke-error.d.ts","sourceRoot":"","sources":["../src/invoke-error.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,WAAY,SAAQ,KAAK;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;gBAEZ,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CA8BzF;AAED,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,GAAG,IAAI,WAAW,CAM9D"}
|
package/dist/invoke-error.js
CHANGED
|
@@ -9,7 +9,7 @@ const INVOKE_ERROR = Symbol.for("telo.InvokeError");
|
|
|
9
9
|
export class InvokeError extends Error {
|
|
10
10
|
code;
|
|
11
11
|
data;
|
|
12
|
-
constructor(code, message, data) {
|
|
12
|
+
constructor(code, message, data, options) {
|
|
13
13
|
super(message);
|
|
14
14
|
this.name = "InvokeError";
|
|
15
15
|
this.code = code;
|
|
@@ -25,6 +25,19 @@ export class InvokeError extends Error {
|
|
|
25
25
|
writable: false,
|
|
26
26
|
configurable: false,
|
|
27
27
|
});
|
|
28
|
+
// Preserve the error being wrapped. Defined rather than assigned so it is
|
|
29
|
+
// non-enumerable — matching how the Error constructor's own `cause` option
|
|
30
|
+
// behaves, and keeping it out of JSON serialisation / CEL property access
|
|
31
|
+
// for the same reason as the marker above. (`ErrorOptions` is not in this
|
|
32
|
+
// package's TS lib, hence the local option type.)
|
|
33
|
+
if (options && "cause" in options) {
|
|
34
|
+
Object.defineProperty(this, "cause", {
|
|
35
|
+
value: options.cause,
|
|
36
|
+
enumerable: false,
|
|
37
|
+
writable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
28
41
|
}
|
|
29
42
|
}
|
|
30
43
|
export function isInvokeError(err) {
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/** Raised when a request never reached the peer — DNS, connect, reset, or TLS
|
|
2
|
+
* trust. Distinct from a non-OK HTTP response, which is a reply the caller
|
|
3
|
+
* interprets itself. */
|
|
4
|
+
export declare const ERR_NETWORK_UNREACHABLE = "ERR_NETWORK_UNREACHABLE";
|
|
5
|
+
/** The facts a transport failure carries. Deliberately structured rather than
|
|
6
|
+
* pre-rendered: a controller reports *what happened*, and whoever displays the
|
|
7
|
+
* error turns it into a sentence. Prose baked into a controller would have to
|
|
8
|
+
* be re-typed identically by every language SDK (TS, Rust, Go, …) and would
|
|
9
|
+
* drift; `cause: "ENOTFOUND"` is the same symbol in every language.
|
|
10
|
+
*
|
|
11
|
+
* `message` on the thrown error is a reasonable default for today's renderers;
|
|
12
|
+
* a kernel-side renderer can format from these fields instead. */
|
|
13
|
+
export interface NetworkErrorData {
|
|
14
|
+
/** What was being attempted, e.g. `"Embedding model request"`. */
|
|
15
|
+
operation: string;
|
|
16
|
+
url: string;
|
|
17
|
+
host: string;
|
|
18
|
+
port?: number;
|
|
19
|
+
/** The OS/undici code — `ENOTFOUND`, `ECONNREFUSED`, `CERT_HAS_EXPIRED`, … */
|
|
20
|
+
cause: string;
|
|
21
|
+
/** The underlying error's own message, kept verbatim. Carries detail no code
|
|
22
|
+
* mapping has (`SSL alert number 80`, a resolver's remarks), so wrapping is
|
|
23
|
+
* never a downgrade on an unmapped code. */
|
|
24
|
+
detail?: string;
|
|
25
|
+
/** `metadata.name` of the resource whose configuration produced `url`, so the
|
|
26
|
+
* error names the actual instance rather than its kind. */
|
|
27
|
+
resource?: string;
|
|
28
|
+
/** The setting to change — a manifest field (`baseUrl`) or a CLI/env name
|
|
29
|
+
* (`--registry`). Structured rather than a pre-written sentence: this is the
|
|
30
|
+
* one genuinely actionable part, and prose here would be the thing every
|
|
31
|
+
* other language SDK has to retype and keep in sync. */
|
|
32
|
+
setting?: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Walk the cause chain for the first `code` — undici wraps the real error one or
|
|
36
|
+
* more levels down, which is exactly the detail lost when only `message` is
|
|
37
|
+
* reported.
|
|
38
|
+
*
|
|
39
|
+
* Exported because classifying a network failure by substring-matching the
|
|
40
|
+
* message does not work: `fetch` rejects with the literal text `"fetch failed"`
|
|
41
|
+
* for DNS, refusal, and TLS alike, so a `message.includes("enotfound")` test
|
|
42
|
+
* silently never matches and every failure collapses into whichever branch is
|
|
43
|
+
* last. Callers with their own error contract should classify on this code
|
|
44
|
+
* rather than on prose.
|
|
45
|
+
*/
|
|
46
|
+
export declare function networkCauseCode(err: unknown): string | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* `fetch` that turns a transport-level failure into an {@link InvokeError}
|
|
49
|
+
* carrying {@link NetworkErrorData}, instead of undici's opaque
|
|
50
|
+
* `TypeError: fetch failed` whose real cause sits unread on `error.cause`.
|
|
51
|
+
*
|
|
52
|
+
* Only *transport* failures are wrapped. A non-OK response is returned
|
|
53
|
+
* untouched, because a status code is a reply the caller interprets (and often
|
|
54
|
+
* renders from the provider's own error body) — so this drops into an existing
|
|
55
|
+
* call site without changing status handling. Cancellation is re-thrown as-is:
|
|
56
|
+
* an aborted request is the caller's intent, not a network fault.
|
|
57
|
+
*
|
|
58
|
+
* @param context.operation What is being attempted, for the message.
|
|
59
|
+
* @param context.resource `metadata.name` of the resource whose configuration
|
|
60
|
+
* produced the URL, so the error names the instance, not just its kind.
|
|
61
|
+
* @param context.setting The manifest field or CLI/env name to change. Passed
|
|
62
|
+
* as a bare identifier, never a sentence — the wording is composed here, in
|
|
63
|
+
* one place, so another language's SDK supplies the same two facts rather
|
|
64
|
+
* than retyping the same English.
|
|
65
|
+
*/
|
|
66
|
+
export declare function fetchOrThrow(input: string | URL, init: RequestInit | undefined, context: {
|
|
67
|
+
operation: string;
|
|
68
|
+
resource?: string;
|
|
69
|
+
setting?: string;
|
|
70
|
+
}): Promise<Response>;
|
|
71
|
+
//# sourceMappingURL=network-fetch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network-fetch.d.ts","sourceRoot":"","sources":["../src/network-fetch.ts"],"names":[],"mappings":"AAGA;;yBAEyB;AACzB,eAAO,MAAM,uBAAuB,4BAA4B,CAAC;AAEjE;;;;;;;mEAOmE;AACnE,MAAM,WAAW,gBAAgB;IAC/B,kEAAkE;IAClE,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd;;iDAE6C;IAC7C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;gEAC4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;6DAGyD;IACzD,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAiDD;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAQjE;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,GAAG,GAAG,EACnB,IAAI,EAAE,WAAW,GAAG,SAAS,EAC7B,OAAO,EAAE;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAClE,OAAO,CAAC,QAAQ,CAAC,CA4CnB"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { isCancellationError } from "./cancellation.js";
|
|
2
|
+
import { InvokeError } from "./invoke-error.js";
|
|
3
|
+
/** Raised when a request never reached the peer — DNS, connect, reset, or TLS
|
|
4
|
+
* trust. Distinct from a non-OK HTTP response, which is a reply the caller
|
|
5
|
+
* interprets itself. */
|
|
6
|
+
export const ERR_NETWORK_UNREACHABLE = "ERR_NETWORK_UNREACHABLE";
|
|
7
|
+
/** Human explanation per transport failure code. Interpolates only the facts
|
|
8
|
+
* already in `NetworkErrorData`, so a renderer in another language can produce
|
|
9
|
+
* the same sentence from the same fields. */
|
|
10
|
+
function explain(code, host, port) {
|
|
11
|
+
const target = port ? `${host}:${port}` : host;
|
|
12
|
+
switch (code) {
|
|
13
|
+
case "ENOTFOUND":
|
|
14
|
+
return `DNS lookup failed for host '${host}' — the name does not resolve`;
|
|
15
|
+
case "EAI_AGAIN":
|
|
16
|
+
return `DNS is temporarily unavailable resolving '${host}' — a transient resolver failure`;
|
|
17
|
+
case "ECONNREFUSED":
|
|
18
|
+
return `nothing is listening on ${target}`;
|
|
19
|
+
case "ECONNRESET":
|
|
20
|
+
return `the connection to ${target} was reset by the peer`;
|
|
21
|
+
case "EHOSTUNREACH":
|
|
22
|
+
return `no network route to ${host}`;
|
|
23
|
+
case "ETIMEDOUT":
|
|
24
|
+
case "UND_ERR_CONNECT_TIMEOUT":
|
|
25
|
+
return `the connection to ${target} timed out`;
|
|
26
|
+
case "CERT_HAS_EXPIRED":
|
|
27
|
+
return `the TLS certificate presented by ${host} has expired`;
|
|
28
|
+
case "DEPTH_ZERO_SELF_SIGNED_CERT":
|
|
29
|
+
case "SELF_SIGNED_CERT_IN_CHAIN":
|
|
30
|
+
return `the TLS certificate presented by ${host} is self-signed and not trusted`;
|
|
31
|
+
case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
|
|
32
|
+
return `the TLS certificate chain presented by ${host} could not be verified`;
|
|
33
|
+
case "EPROTO":
|
|
34
|
+
return `the TLS handshake with ${host} failed`;
|
|
35
|
+
default:
|
|
36
|
+
return `the request to ${target} failed at the transport layer`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** The deepest `message` in the cause chain — the one carrying detail a code
|
|
40
|
+
* mapping cannot have. Skips undici's own `"fetch failed"`, which is the
|
|
41
|
+
* placeholder this whole module exists to replace. */
|
|
42
|
+
function causeDetail(err) {
|
|
43
|
+
let current = err;
|
|
44
|
+
let detail;
|
|
45
|
+
for (let depth = 0; current && depth < 5; depth++) {
|
|
46
|
+
const message = current.message;
|
|
47
|
+
if (typeof message === "string" && message && message !== "fetch failed")
|
|
48
|
+
detail = message;
|
|
49
|
+
current = current.cause;
|
|
50
|
+
}
|
|
51
|
+
return detail;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Walk the cause chain for the first `code` — undici wraps the real error one or
|
|
55
|
+
* more levels down, which is exactly the detail lost when only `message` is
|
|
56
|
+
* reported.
|
|
57
|
+
*
|
|
58
|
+
* Exported because classifying a network failure by substring-matching the
|
|
59
|
+
* message does not work: `fetch` rejects with the literal text `"fetch failed"`
|
|
60
|
+
* for DNS, refusal, and TLS alike, so a `message.includes("enotfound")` test
|
|
61
|
+
* silently never matches and every failure collapses into whichever branch is
|
|
62
|
+
* last. Callers with their own error contract should classify on this code
|
|
63
|
+
* rather than on prose.
|
|
64
|
+
*/
|
|
65
|
+
export function networkCauseCode(err) {
|
|
66
|
+
let current = err;
|
|
67
|
+
for (let depth = 0; current && depth < 5; depth++) {
|
|
68
|
+
const code = current.code;
|
|
69
|
+
if (typeof code === "string")
|
|
70
|
+
return code;
|
|
71
|
+
current = current.cause;
|
|
72
|
+
}
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* `fetch` that turns a transport-level failure into an {@link InvokeError}
|
|
77
|
+
* carrying {@link NetworkErrorData}, instead of undici's opaque
|
|
78
|
+
* `TypeError: fetch failed` whose real cause sits unread on `error.cause`.
|
|
79
|
+
*
|
|
80
|
+
* Only *transport* failures are wrapped. A non-OK response is returned
|
|
81
|
+
* untouched, because a status code is a reply the caller interprets (and often
|
|
82
|
+
* renders from the provider's own error body) — so this drops into an existing
|
|
83
|
+
* call site without changing status handling. Cancellation is re-thrown as-is:
|
|
84
|
+
* an aborted request is the caller's intent, not a network fault.
|
|
85
|
+
*
|
|
86
|
+
* @param context.operation What is being attempted, for the message.
|
|
87
|
+
* @param context.resource `metadata.name` of the resource whose configuration
|
|
88
|
+
* produced the URL, so the error names the instance, not just its kind.
|
|
89
|
+
* @param context.setting The manifest field or CLI/env name to change. Passed
|
|
90
|
+
* as a bare identifier, never a sentence — the wording is composed here, in
|
|
91
|
+
* one place, so another language's SDK supplies the same two facts rather
|
|
92
|
+
* than retyping the same English.
|
|
93
|
+
*/
|
|
94
|
+
export async function fetchOrThrow(input, init, context) {
|
|
95
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
96
|
+
try {
|
|
97
|
+
return await fetch(input, init);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
if (isCancellationError(err))
|
|
101
|
+
throw err;
|
|
102
|
+
if (err instanceof DOMException && err.name === "AbortError")
|
|
103
|
+
throw err;
|
|
104
|
+
const code = networkCauseCode(err) ?? "UNKNOWN";
|
|
105
|
+
let host = url;
|
|
106
|
+
let port;
|
|
107
|
+
try {
|
|
108
|
+
const parsed = new URL(url);
|
|
109
|
+
host = parsed.hostname;
|
|
110
|
+
if (parsed.port)
|
|
111
|
+
port = Number(parsed.port);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Non-absolute URL — fall back to the raw string as the host label.
|
|
115
|
+
}
|
|
116
|
+
const detail = causeDetail(err);
|
|
117
|
+
const data = { operation: context.operation, url, host, cause: code };
|
|
118
|
+
if (port !== undefined)
|
|
119
|
+
data.port = port;
|
|
120
|
+
if (detail)
|
|
121
|
+
data.detail = detail;
|
|
122
|
+
if (context.resource)
|
|
123
|
+
data.resource = context.resource;
|
|
124
|
+
if (context.setting)
|
|
125
|
+
data.setting = context.setting;
|
|
126
|
+
// `detail` is appended only when the code has no mapping of its own —
|
|
127
|
+
// otherwise the explanation already says it better. Without this, wrapping
|
|
128
|
+
// an unmapped code would *lose* information relative to the raw error.
|
|
129
|
+
const explained = explain(code, host, port);
|
|
130
|
+
const isMapped = !explained.startsWith("the request to");
|
|
131
|
+
const because = isMapped || !detail ? explained : `${explained} (${detail})`;
|
|
132
|
+
const fix = context.setting
|
|
133
|
+
? ` Check \`${context.setting}\`${context.resource ? ` on resource '${context.resource}'` : ""}.`
|
|
134
|
+
: "";
|
|
135
|
+
const message = `${context.operation} failed: cannot reach ${url} — ${code}: ${because}.${fix}`;
|
|
136
|
+
// The wrapped error stays reachable as `cause`: the code mapping is a
|
|
137
|
+
// convenience, never a reason to destroy what was actually thrown.
|
|
138
|
+
throw new InvokeError(ERR_NETWORK_UNREACHABLE, message, data, { cause: err });
|
|
139
|
+
}
|
|
140
|
+
}
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export * from "./resource-context.js";
|
|
|
17
17
|
export * from "./resource-instance.js";
|
|
18
18
|
export * from "./resource-manifest.js";
|
|
19
19
|
export * from "./invoke-error.js";
|
|
20
|
+
export * from "./network-fetch.js";
|
|
20
21
|
export * from "./runtime-error.js";
|
|
21
22
|
export * from "./runtime-event.js";
|
|
22
23
|
export * from "./runtime-resource.js";
|
package/src/invoke-error.ts
CHANGED
|
@@ -11,7 +11,7 @@ export class InvokeError extends Error {
|
|
|
11
11
|
readonly code: string;
|
|
12
12
|
readonly data?: unknown;
|
|
13
13
|
|
|
14
|
-
constructor(code: string, message: string, data?: unknown) {
|
|
14
|
+
constructor(code: string, message: string, data?: unknown, options?: { cause?: unknown }) {
|
|
15
15
|
super(message);
|
|
16
16
|
this.name = "InvokeError";
|
|
17
17
|
this.code = code;
|
|
@@ -27,6 +27,19 @@ export class InvokeError extends Error {
|
|
|
27
27
|
writable: false,
|
|
28
28
|
configurable: false,
|
|
29
29
|
});
|
|
30
|
+
// Preserve the error being wrapped. Defined rather than assigned so it is
|
|
31
|
+
// non-enumerable — matching how the Error constructor's own `cause` option
|
|
32
|
+
// behaves, and keeping it out of JSON serialisation / CEL property access
|
|
33
|
+
// for the same reason as the marker above. (`ErrorOptions` is not in this
|
|
34
|
+
// package's TS lib, hence the local option type.)
|
|
35
|
+
if (options && "cause" in options) {
|
|
36
|
+
Object.defineProperty(this, "cause", {
|
|
37
|
+
value: options.cause,
|
|
38
|
+
enumerable: false,
|
|
39
|
+
writable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
30
43
|
}
|
|
31
44
|
}
|
|
32
45
|
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { isCancellationError } from "./cancellation.js";
|
|
2
|
+
import { InvokeError } from "./invoke-error.js";
|
|
3
|
+
|
|
4
|
+
/** Raised when a request never reached the peer — DNS, connect, reset, or TLS
|
|
5
|
+
* trust. Distinct from a non-OK HTTP response, which is a reply the caller
|
|
6
|
+
* interprets itself. */
|
|
7
|
+
export const ERR_NETWORK_UNREACHABLE = "ERR_NETWORK_UNREACHABLE";
|
|
8
|
+
|
|
9
|
+
/** The facts a transport failure carries. Deliberately structured rather than
|
|
10
|
+
* pre-rendered: a controller reports *what happened*, and whoever displays the
|
|
11
|
+
* error turns it into a sentence. Prose baked into a controller would have to
|
|
12
|
+
* be re-typed identically by every language SDK (TS, Rust, Go, …) and would
|
|
13
|
+
* drift; `cause: "ENOTFOUND"` is the same symbol in every language.
|
|
14
|
+
*
|
|
15
|
+
* `message` on the thrown error is a reasonable default for today's renderers;
|
|
16
|
+
* a kernel-side renderer can format from these fields instead. */
|
|
17
|
+
export interface NetworkErrorData {
|
|
18
|
+
/** What was being attempted, e.g. `"Embedding model request"`. */
|
|
19
|
+
operation: string;
|
|
20
|
+
url: string;
|
|
21
|
+
host: string;
|
|
22
|
+
port?: number;
|
|
23
|
+
/** The OS/undici code — `ENOTFOUND`, `ECONNREFUSED`, `CERT_HAS_EXPIRED`, … */
|
|
24
|
+
cause: string;
|
|
25
|
+
/** The underlying error's own message, kept verbatim. Carries detail no code
|
|
26
|
+
* mapping has (`SSL alert number 80`, a resolver's remarks), so wrapping is
|
|
27
|
+
* never a downgrade on an unmapped code. */
|
|
28
|
+
detail?: string;
|
|
29
|
+
/** `metadata.name` of the resource whose configuration produced `url`, so the
|
|
30
|
+
* error names the actual instance rather than its kind. */
|
|
31
|
+
resource?: string;
|
|
32
|
+
/** The setting to change — a manifest field (`baseUrl`) or a CLI/env name
|
|
33
|
+
* (`--registry`). Structured rather than a pre-written sentence: this is the
|
|
34
|
+
* one genuinely actionable part, and prose here would be the thing every
|
|
35
|
+
* other language SDK has to retype and keep in sync. */
|
|
36
|
+
setting?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Human explanation per transport failure code. Interpolates only the facts
|
|
40
|
+
* already in `NetworkErrorData`, so a renderer in another language can produce
|
|
41
|
+
* the same sentence from the same fields. */
|
|
42
|
+
function explain(code: string, host: string, port?: number): string {
|
|
43
|
+
const target = port ? `${host}:${port}` : host;
|
|
44
|
+
switch (code) {
|
|
45
|
+
case "ENOTFOUND":
|
|
46
|
+
return `DNS lookup failed for host '${host}' — the name does not resolve`;
|
|
47
|
+
case "EAI_AGAIN":
|
|
48
|
+
return `DNS is temporarily unavailable resolving '${host}' — a transient resolver failure`;
|
|
49
|
+
case "ECONNREFUSED":
|
|
50
|
+
return `nothing is listening on ${target}`;
|
|
51
|
+
case "ECONNRESET":
|
|
52
|
+
return `the connection to ${target} was reset by the peer`;
|
|
53
|
+
case "EHOSTUNREACH":
|
|
54
|
+
return `no network route to ${host}`;
|
|
55
|
+
case "ETIMEDOUT":
|
|
56
|
+
case "UND_ERR_CONNECT_TIMEOUT":
|
|
57
|
+
return `the connection to ${target} timed out`;
|
|
58
|
+
case "CERT_HAS_EXPIRED":
|
|
59
|
+
return `the TLS certificate presented by ${host} has expired`;
|
|
60
|
+
case "DEPTH_ZERO_SELF_SIGNED_CERT":
|
|
61
|
+
case "SELF_SIGNED_CERT_IN_CHAIN":
|
|
62
|
+
return `the TLS certificate presented by ${host} is self-signed and not trusted`;
|
|
63
|
+
case "UNABLE_TO_VERIFY_LEAF_SIGNATURE":
|
|
64
|
+
return `the TLS certificate chain presented by ${host} could not be verified`;
|
|
65
|
+
case "EPROTO":
|
|
66
|
+
return `the TLS handshake with ${host} failed`;
|
|
67
|
+
default:
|
|
68
|
+
return `the request to ${target} failed at the transport layer`;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The deepest `message` in the cause chain — the one carrying detail a code
|
|
73
|
+
* mapping cannot have. Skips undici's own `"fetch failed"`, which is the
|
|
74
|
+
* placeholder this whole module exists to replace. */
|
|
75
|
+
function causeDetail(err: unknown): string | undefined {
|
|
76
|
+
let current: unknown = err;
|
|
77
|
+
let detail: string | undefined;
|
|
78
|
+
for (let depth = 0; current && depth < 5; depth++) {
|
|
79
|
+
const message = (current as { message?: unknown }).message;
|
|
80
|
+
if (typeof message === "string" && message && message !== "fetch failed") detail = message;
|
|
81
|
+
current = (current as { cause?: unknown }).cause;
|
|
82
|
+
}
|
|
83
|
+
return detail;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Walk the cause chain for the first `code` — undici wraps the real error one or
|
|
88
|
+
* more levels down, which is exactly the detail lost when only `message` is
|
|
89
|
+
* reported.
|
|
90
|
+
*
|
|
91
|
+
* Exported because classifying a network failure by substring-matching the
|
|
92
|
+
* message does not work: `fetch` rejects with the literal text `"fetch failed"`
|
|
93
|
+
* for DNS, refusal, and TLS alike, so a `message.includes("enotfound")` test
|
|
94
|
+
* silently never matches and every failure collapses into whichever branch is
|
|
95
|
+
* last. Callers with their own error contract should classify on this code
|
|
96
|
+
* rather than on prose.
|
|
97
|
+
*/
|
|
98
|
+
export function networkCauseCode(err: unknown): string | undefined {
|
|
99
|
+
let current: unknown = err;
|
|
100
|
+
for (let depth = 0; current && depth < 5; depth++) {
|
|
101
|
+
const code = (current as { code?: unknown }).code;
|
|
102
|
+
if (typeof code === "string") return code;
|
|
103
|
+
current = (current as { cause?: unknown }).cause;
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* `fetch` that turns a transport-level failure into an {@link InvokeError}
|
|
110
|
+
* carrying {@link NetworkErrorData}, instead of undici's opaque
|
|
111
|
+
* `TypeError: fetch failed` whose real cause sits unread on `error.cause`.
|
|
112
|
+
*
|
|
113
|
+
* Only *transport* failures are wrapped. A non-OK response is returned
|
|
114
|
+
* untouched, because a status code is a reply the caller interprets (and often
|
|
115
|
+
* renders from the provider's own error body) — so this drops into an existing
|
|
116
|
+
* call site without changing status handling. Cancellation is re-thrown as-is:
|
|
117
|
+
* an aborted request is the caller's intent, not a network fault.
|
|
118
|
+
*
|
|
119
|
+
* @param context.operation What is being attempted, for the message.
|
|
120
|
+
* @param context.resource `metadata.name` of the resource whose configuration
|
|
121
|
+
* produced the URL, so the error names the instance, not just its kind.
|
|
122
|
+
* @param context.setting The manifest field or CLI/env name to change. Passed
|
|
123
|
+
* as a bare identifier, never a sentence — the wording is composed here, in
|
|
124
|
+
* one place, so another language's SDK supplies the same two facts rather
|
|
125
|
+
* than retyping the same English.
|
|
126
|
+
*/
|
|
127
|
+
export async function fetchOrThrow(
|
|
128
|
+
input: string | URL,
|
|
129
|
+
init: RequestInit | undefined,
|
|
130
|
+
context: { operation: string; resource?: string; setting?: string },
|
|
131
|
+
): Promise<Response> {
|
|
132
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
133
|
+
try {
|
|
134
|
+
return await fetch(input, init);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (isCancellationError(err)) throw err;
|
|
137
|
+
if (err instanceof DOMException && err.name === "AbortError") throw err;
|
|
138
|
+
|
|
139
|
+
const code = networkCauseCode(err) ?? "UNKNOWN";
|
|
140
|
+
let host = url;
|
|
141
|
+
let port: number | undefined;
|
|
142
|
+
try {
|
|
143
|
+
const parsed = new URL(url);
|
|
144
|
+
host = parsed.hostname;
|
|
145
|
+
if (parsed.port) port = Number(parsed.port);
|
|
146
|
+
} catch {
|
|
147
|
+
// Non-absolute URL — fall back to the raw string as the host label.
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const detail = causeDetail(err);
|
|
151
|
+
const data: NetworkErrorData = { operation: context.operation, url, host, cause: code };
|
|
152
|
+
if (port !== undefined) data.port = port;
|
|
153
|
+
if (detail) data.detail = detail;
|
|
154
|
+
if (context.resource) data.resource = context.resource;
|
|
155
|
+
if (context.setting) data.setting = context.setting;
|
|
156
|
+
|
|
157
|
+
// `detail` is appended only when the code has no mapping of its own —
|
|
158
|
+
// otherwise the explanation already says it better. Without this, wrapping
|
|
159
|
+
// an unmapped code would *lose* information relative to the raw error.
|
|
160
|
+
const explained = explain(code, host, port);
|
|
161
|
+
const isMapped = !explained.startsWith("the request to");
|
|
162
|
+
const because = isMapped || !detail ? explained : `${explained} (${detail})`;
|
|
163
|
+
|
|
164
|
+
const fix = context.setting
|
|
165
|
+
? ` Check \`${context.setting}\`${context.resource ? ` on resource '${context.resource}'` : ""}.`
|
|
166
|
+
: "";
|
|
167
|
+
|
|
168
|
+
const message =
|
|
169
|
+
`${context.operation} failed: cannot reach ${url} — ${code}: ${because}.${fix}`;
|
|
170
|
+
|
|
171
|
+
// The wrapped error stays reachable as `cause`: the code mapping is a
|
|
172
|
+
// convenience, never a reason to destroy what was actually thrown.
|
|
173
|
+
throw new InvokeError(ERR_NETWORK_UNREACHABLE, message, data, { cause: err });
|
|
174
|
+
}
|
|
175
|
+
}
|