@apifuse/provider-sdk 2.2.0-beta.18 → 2.2.0-beta.20
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/CHANGELOG.md +9 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/observability.d.ts +5 -2
- package/dist/observability.js +48 -1
- package/dist/server/serve.d.ts +1 -0
- package/dist/server/serve.js +51 -6
- package/dist/server/types.d.ts +18 -0
- package/dist/server/types.js +5 -0
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/observability.ts +51 -1
- package/src/server/serve.ts +57 -5
- package/src/server/types.ts +5 -0
- package/src/types.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# @apifuse/provider-sdk Changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.0-beta.20
|
|
4
|
+
|
|
5
|
+
- Release candidate for main commit 8043d2ef0047e431aace750d8abca2e1149ec1d6.
|
|
6
|
+
|
|
7
|
+
## 2.2.0-beta.19
|
|
8
|
+
|
|
9
|
+
- Release candidate for main commit fd93ae68d0472fae68d908312249163373ec5d22.
|
|
10
|
+
|
|
3
11
|
## 2.2.0-beta.18
|
|
4
12
|
|
|
5
13
|
- Release candidate for main commit cebaf4b994918d2641c0fe6681fbffd3a3284c5d.
|
|
@@ -102,6 +110,7 @@
|
|
|
102
110
|
|
|
103
111
|
## Unreleased
|
|
104
112
|
|
|
113
|
+
- **honest-provider-error-contract (phase 2):** `UPSTREAM_REJECTED` is a registered code family serving HTTP 409 with `retryable: false` — deterministic upstream business refusals are no longer 502s. Operation-declared `docs.errorCodes` may now use 409/410/422. Public error envelopes carry a `source` field (`client` | `upstream_rule` | `upstream_failure` | `apifuse`) derived from the observability category. Taxonomy version bumps to `2026-08-07` with the `upstream_rejected`, `dependency_unavailable`, `unsupported_transport`, and `client_cancelled` categories (409/410/422 map to `upstream_rejected`), matching the platform monorepo SoT. The `unregistered_provider_error_code` signal now carries a `signalFix` pointing at `docs.errorCodes` declaration.
|
|
105
114
|
- **Breaking for custom native gateway adapters:** `NativeGatewayProxySynthesisInput` now includes an injected `credentials` resolver and selected `protocol`; synthesizers may return promises and structured skip reasons, and `resolveNativeGatewayProxy` is async. Default callers retain env-backed behavior. Native transport now supports both HTTP CONNECT and SOCKS5, defaults per vendor with an explicit runtime override, registers smartproxy allocation ahead of nodemaven when declared in that order, and reports every exhausted vendor reason without exposing proxy credentials.
|
|
106
115
|
- Honor operation `docs.errorCodes` at runtime: declared provider-owned statuses and retryability now drive the HTTP envelope, observability header, and structured log; invalid statuses fail `defineProvider`, declared codes no longer emit the unregistered-code signal, and `TransportError` status-preservation workarounds are obsolete.
|
|
107
116
|
- Add an opt-in same-origin redirect hop policy to `ctx.http`, with bounded manual following and typed failures before a refused target is requested.
|
package/dist/index.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export { centered, delayed, defineHealthJourney, defineOperation, defineProvider
|
|
|
8
8
|
export type { DevServerOptions } from "./dev.js";
|
|
9
9
|
export { createDevServer, startDevServer } from "./dev.js";
|
|
10
10
|
export * from "./errors.js";
|
|
11
|
+
export * from "./observability.js";
|
|
11
12
|
export * from "./user-input.js";
|
|
12
13
|
export * from "./i18n/index.js";
|
|
13
14
|
export { type LintDiagnostic, lintOperation, lintProvider, } from "./lint.js";
|
package/dist/index.js
CHANGED
|
@@ -7,6 +7,7 @@ export { canonicalJson, digestProviderContract, extractProviderContract, PROVIDE
|
|
|
7
7
|
export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, } from "./define.js";
|
|
8
8
|
export { createDevServer, startDevServer } from "./dev.js";
|
|
9
9
|
export * from "./errors.js";
|
|
10
|
+
export * from "./observability.js";
|
|
10
11
|
export * from "./user-input.js";
|
|
11
12
|
export * from "./i18n/index.js";
|
|
12
13
|
export { lintOperation, lintProvider, } from "./lint.js";
|
package/dist/observability.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
export declare const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-
|
|
2
|
-
export declare const PROVIDER_ERROR_CATEGORIES: readonly ["ok", "timeout", "network", "upstream_http", "upstream_rate_limited", "upstream_auth", "upstream_schema_drift", "proxy_pool", "anti_bot_blocked", "credential_expired", "credential_unavailable", "input_validation", "output_validation", "provider_error", "internal_error", "unclassified"];
|
|
1
|
+
export declare const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-08-07";
|
|
2
|
+
export declare const PROVIDER_ERROR_CATEGORIES: readonly ["ok", "timeout", "network", "upstream_http", "upstream_rate_limited", "upstream_auth", "upstream_rejected", "upstream_schema_drift", "proxy_pool", "anti_bot_blocked", "credential_expired", "credential_unavailable", "input_validation", "output_validation", "provider_error", "internal_error", "dependency_unavailable", "unsupported_transport", "client_cancelled", "unclassified"];
|
|
3
3
|
export type ProviderErrorCategory = (typeof PROVIDER_ERROR_CATEGORIES)[number];
|
|
4
4
|
export declare function categoryForStatus(status: number): ProviderErrorCategory;
|
|
5
5
|
export declare function isRetryableCategory(category: ProviderErrorCategory): boolean;
|
|
6
|
+
export declare const PROVIDER_ERROR_SOURCES: readonly ["client", "upstream_rule", "upstream_failure", "apifuse"];
|
|
7
|
+
export type ProviderErrorSource = (typeof PROVIDER_ERROR_SOURCES)[number];
|
|
8
|
+
export declare function sourceForCategory(category: ProviderErrorCategory): ProviderErrorSource;
|
package/dist/observability.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// Mirrors packages/provider-observability in the platform monorepo (SoT).
|
|
2
|
+
export const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-08-07";
|
|
2
3
|
export const PROVIDER_ERROR_CATEGORIES = [
|
|
3
4
|
"ok",
|
|
4
5
|
"timeout",
|
|
@@ -6,6 +7,7 @@ export const PROVIDER_ERROR_CATEGORIES = [
|
|
|
6
7
|
"upstream_http",
|
|
7
8
|
"upstream_rate_limited",
|
|
8
9
|
"upstream_auth",
|
|
10
|
+
"upstream_rejected",
|
|
9
11
|
"upstream_schema_drift",
|
|
10
12
|
"proxy_pool",
|
|
11
13
|
"anti_bot_blocked",
|
|
@@ -15,6 +17,9 @@ export const PROVIDER_ERROR_CATEGORIES = [
|
|
|
15
17
|
"output_validation",
|
|
16
18
|
"provider_error",
|
|
17
19
|
"internal_error",
|
|
20
|
+
"dependency_unavailable",
|
|
21
|
+
"unsupported_transport",
|
|
22
|
+
"client_cancelled",
|
|
18
23
|
"unclassified",
|
|
19
24
|
];
|
|
20
25
|
export function categoryForStatus(status) {
|
|
@@ -26,6 +31,10 @@ export function categoryForStatus(status) {
|
|
|
26
31
|
return "upstream_rate_limited";
|
|
27
32
|
if (status === 401 || status === 403)
|
|
28
33
|
return "upstream_auth";
|
|
34
|
+
// provider-error-contract: 409/410/422 are the deterministic
|
|
35
|
+
// upstream-rejection status class — not a retryable upstream failure.
|
|
36
|
+
if (status === 409 || status === 410 || status === 422)
|
|
37
|
+
return "upstream_rejected";
|
|
29
38
|
if (status >= 400)
|
|
30
39
|
return "upstream_http";
|
|
31
40
|
return "unclassified";
|
|
@@ -37,3 +46,41 @@ export function isRetryableCategory(category) {
|
|
|
37
46
|
category === "upstream_http" ||
|
|
38
47
|
category === "proxy_pool");
|
|
39
48
|
}
|
|
49
|
+
// Who stopped the request (honest-provider-error-contract). A deliberately
|
|
50
|
+
// small public projection of the internal taxonomy:
|
|
51
|
+
// - client: the request itself is fixable by the caller
|
|
52
|
+
// - upstream_rule: the upstream service refused it under its own business
|
|
53
|
+
// rules (deterministic) or rate limits
|
|
54
|
+
// - upstream_failure: the upstream service malfunctioned — retryable
|
|
55
|
+
// - apifuse: an APIFuse-side fault (provider bug, platform dependency)
|
|
56
|
+
export const PROVIDER_ERROR_SOURCES = [
|
|
57
|
+
"client",
|
|
58
|
+
"upstream_rule",
|
|
59
|
+
"upstream_failure",
|
|
60
|
+
"apifuse",
|
|
61
|
+
];
|
|
62
|
+
export function sourceForCategory(category) {
|
|
63
|
+
switch (category) {
|
|
64
|
+
case "input_validation":
|
|
65
|
+
case "credential_expired":
|
|
66
|
+
case "credential_unavailable":
|
|
67
|
+
case "client_cancelled":
|
|
68
|
+
return "client";
|
|
69
|
+
case "upstream_rejected":
|
|
70
|
+
case "upstream_rate_limited":
|
|
71
|
+
return "upstream_rule";
|
|
72
|
+
case "timeout":
|
|
73
|
+
case "network":
|
|
74
|
+
case "upstream_http":
|
|
75
|
+
case "upstream_auth":
|
|
76
|
+
case "upstream_schema_drift":
|
|
77
|
+
case "anti_bot_blocked":
|
|
78
|
+
return "upstream_failure";
|
|
79
|
+
default:
|
|
80
|
+
// ok never reaches error serialization; provider_error,
|
|
81
|
+
// internal_error, output_validation, proxy_pool,
|
|
82
|
+
// dependency_unavailable, unsupported_transport, unclassified are
|
|
83
|
+
// APIFuse-side faults.
|
|
84
|
+
return "apifuse";
|
|
85
|
+
}
|
|
86
|
+
}
|
package/dist/server/serve.d.ts
CHANGED
package/dist/server/serve.js
CHANGED
|
@@ -6,7 +6,7 @@ import { AuthAbortError, createAuthFlowHelpers } from "../auth.js";
|
|
|
6
6
|
import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, } from "../error-resolution.js";
|
|
7
7
|
import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
|
|
8
8
|
import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
|
|
9
|
-
import { categoryForStatus, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
|
|
9
|
+
import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
|
|
10
10
|
import { createScratchpad } from "../runtime/auth-flow.js";
|
|
11
11
|
import { createBrowserClient } from "../runtime/browser.js";
|
|
12
12
|
import { createProviderCache } from "../runtime/cache.js";
|
|
@@ -357,8 +357,28 @@ function zodDetails(error) {
|
|
|
357
357
|
message: issue.message,
|
|
358
358
|
}));
|
|
359
359
|
}
|
|
360
|
+
// Category-level projection with code-aware honesty overrides: a missing
|
|
361
|
+
// deployment secret is an APIFuse-side defect even though its category
|
|
362
|
+
// (credential_unavailable) usually means a caller credential problem, an
|
|
363
|
+
// internal stateful-routing deadline is APIFuse-owned despite its timeout
|
|
364
|
+
// category, and the built-in upstream failure families keep their upstream
|
|
365
|
+
// attribution even when the author left the category at the provider_error
|
|
366
|
+
// default.
|
|
367
|
+
function publicErrorSource(error, category) {
|
|
368
|
+
if (error instanceof StatefulRoutingDeadlineError)
|
|
369
|
+
return "apifuse";
|
|
370
|
+
if (isProviderError(error)) {
|
|
371
|
+
if (error.code === MISSING_SECRET_CODE)
|
|
372
|
+
return "apifuse";
|
|
373
|
+
if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
|
|
374
|
+
return "upstream_failure";
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return sourceForCategory(category);
|
|
378
|
+
}
|
|
360
379
|
function toErrorResponse(error, requestId, declaredErrorCode) {
|
|
361
380
|
const observability = errorObservabilityDetails(error, declaredErrorCode);
|
|
381
|
+
const source = publicErrorSource(error, observability.category);
|
|
362
382
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
363
383
|
return {
|
|
364
384
|
error: {
|
|
@@ -366,6 +386,7 @@ function toErrorResponse(error, requestId, declaredErrorCode) {
|
|
|
366
386
|
message: "Stateful forwarding deadline expired.",
|
|
367
387
|
...(requestId ? { requestId } : {}),
|
|
368
388
|
retryable: observability.retryable,
|
|
389
|
+
source,
|
|
369
390
|
},
|
|
370
391
|
};
|
|
371
392
|
}
|
|
@@ -377,6 +398,7 @@ function toErrorResponse(error, requestId, declaredErrorCode) {
|
|
|
377
398
|
message: publicProviderErrorMessage(error),
|
|
378
399
|
...(requestId ? { requestId } : {}),
|
|
379
400
|
retryable: observability.retryable,
|
|
401
|
+
source,
|
|
380
402
|
...(error.fix ? { fix: error.fix } : {}),
|
|
381
403
|
...(details !== undefined ? { details } : {}),
|
|
382
404
|
},
|
|
@@ -389,6 +411,7 @@ function toErrorResponse(error, requestId, declaredErrorCode) {
|
|
|
389
411
|
message: "Invalid request body",
|
|
390
412
|
...(requestId ? { requestId } : {}),
|
|
391
413
|
retryable: observability.retryable,
|
|
414
|
+
source,
|
|
392
415
|
details: zodDetails(error),
|
|
393
416
|
},
|
|
394
417
|
};
|
|
@@ -405,6 +428,7 @@ function toErrorResponse(error, requestId, declaredErrorCode) {
|
|
|
405
428
|
message: "Internal error",
|
|
406
429
|
...(requestId ? { requestId } : {}),
|
|
407
430
|
retryable: observability.retryable,
|
|
431
|
+
source,
|
|
408
432
|
details: {
|
|
409
433
|
retryable: false,
|
|
410
434
|
category: "internal_error",
|
|
@@ -484,9 +508,12 @@ function errorObservabilityDetails(error, declaredErrorCode) {
|
|
|
484
508
|
return {
|
|
485
509
|
category: isProviderError(error) && error.options?.category
|
|
486
510
|
? error.options.category
|
|
487
|
-
: isEmittableErrorStatus(declaredStatus) &&
|
|
488
|
-
|
|
489
|
-
|
|
511
|
+
: isEmittableErrorStatus(declaredStatus) &&
|
|
512
|
+
categoryForStatus(declaredStatus) === "upstream_rejected"
|
|
513
|
+
? "upstream_rejected"
|
|
514
|
+
: isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
|
|
515
|
+
? "provider_error"
|
|
516
|
+
: "input_validation",
|
|
490
517
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
491
518
|
retryable: isProviderError(error)
|
|
492
519
|
? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
|
|
@@ -501,8 +528,18 @@ function errorObservabilityDetails(error, declaredErrorCode) {
|
|
|
501
528
|
};
|
|
502
529
|
}
|
|
503
530
|
if (isProviderError(error)) {
|
|
531
|
+
// Deterministic upstream refusals default to the rejection category:
|
|
532
|
+
// the UPSTREAM_REJECTED family and any operation-declared rejection
|
|
533
|
+
// status (409/410/422) classify as upstream_rejected unless the
|
|
534
|
+
// author set an explicit category.
|
|
535
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
536
|
+
const rejectionDefault = error.code === "UPSTREAM_REJECTED" ||
|
|
537
|
+
(isEmittableErrorStatus(declaredStatus) &&
|
|
538
|
+
categoryForStatus(declaredStatus) === "upstream_rejected")
|
|
539
|
+
? "upstream_rejected"
|
|
540
|
+
: "provider_error";
|
|
504
541
|
return {
|
|
505
|
-
category: error.options?.category ??
|
|
542
|
+
category: error.options?.category ?? rejectionDefault,
|
|
506
543
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
507
544
|
retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
|
|
508
545
|
};
|
|
@@ -579,6 +616,11 @@ function toStatusCode(error, declaredErrorCode) {
|
|
|
579
616
|
case "UPSTREAM_RATE_LIMIT":
|
|
580
617
|
case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
|
|
581
618
|
return 429;
|
|
619
|
+
// Deterministic upstream business refusal (honest-provider-error-
|
|
620
|
+
// contract): the upstream evaluated the request and said no under
|
|
621
|
+
// its own rules — a conflict with upstream state, never a 5xx.
|
|
622
|
+
case "UPSTREAM_REJECTED":
|
|
623
|
+
return 409;
|
|
582
624
|
case "UPSTREAM_ERROR":
|
|
583
625
|
case "BLOCKED":
|
|
584
626
|
return 502;
|
|
@@ -665,7 +707,10 @@ function logProviderError(logger, provider, kind, route, requestId, error, statu
|
|
|
665
707
|
taxonomyVersion: details.taxonomyVersion,
|
|
666
708
|
retryable: details.retryable,
|
|
667
709
|
...(isUnregisteredProviderErrorCode
|
|
668
|
-
? {
|
|
710
|
+
? {
|
|
711
|
+
signal: "unregistered_provider_error_code",
|
|
712
|
+
signalFix: "Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
|
|
713
|
+
}
|
|
669
714
|
: {}),
|
|
670
715
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
671
716
|
});
|
package/dist/server/types.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ export declare const ErrorEnvelopeSchema: z.ZodObject<{
|
|
|
43
43
|
message: z.ZodString;
|
|
44
44
|
requestId: z.ZodOptional<z.ZodString>;
|
|
45
45
|
retryable: z.ZodBoolean;
|
|
46
|
+
source: z.ZodOptional<z.ZodEnum<{
|
|
47
|
+
client: "client";
|
|
48
|
+
upstream_rule: "upstream_rule";
|
|
49
|
+
upstream_failure: "upstream_failure";
|
|
50
|
+
apifuse: "apifuse";
|
|
51
|
+
}>>;
|
|
46
52
|
fix: z.ZodOptional<z.ZodString>;
|
|
47
53
|
details: z.ZodOptional<z.ZodUnknown>;
|
|
48
54
|
}, z.core.$strip>;
|
|
@@ -86,6 +92,12 @@ export declare const OperationErrorResponseSchema: z.ZodObject<{
|
|
|
86
92
|
message: z.ZodString;
|
|
87
93
|
requestId: z.ZodOptional<z.ZodString>;
|
|
88
94
|
retryable: z.ZodBoolean;
|
|
95
|
+
source: z.ZodOptional<z.ZodEnum<{
|
|
96
|
+
client: "client";
|
|
97
|
+
upstream_rule: "upstream_rule";
|
|
98
|
+
upstream_failure: "upstream_failure";
|
|
99
|
+
apifuse: "apifuse";
|
|
100
|
+
}>>;
|
|
89
101
|
fix: z.ZodOptional<z.ZodString>;
|
|
90
102
|
details: z.ZodOptional<z.ZodUnknown>;
|
|
91
103
|
}, z.core.$strip>;
|
|
@@ -124,6 +136,12 @@ export declare const AuthFlowErrorResponseSchema: z.ZodObject<{
|
|
|
124
136
|
message: z.ZodString;
|
|
125
137
|
requestId: z.ZodOptional<z.ZodString>;
|
|
126
138
|
retryable: z.ZodBoolean;
|
|
139
|
+
source: z.ZodOptional<z.ZodEnum<{
|
|
140
|
+
client: "client";
|
|
141
|
+
upstream_rule: "upstream_rule";
|
|
142
|
+
upstream_failure: "upstream_failure";
|
|
143
|
+
apifuse: "apifuse";
|
|
144
|
+
}>>;
|
|
127
145
|
fix: z.ZodOptional<z.ZodString>;
|
|
128
146
|
details: z.ZodOptional<z.ZodUnknown>;
|
|
129
147
|
}, z.core.$strip>;
|
package/dist/server/types.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { PROVIDER_ERROR_SOURCES } from "../observability.js";
|
|
2
3
|
import { HttpRetryPreset } from "../types.js";
|
|
3
4
|
export const ConnectionModeSchema = z.enum(["oauth2", "credentials", "platform-managed", "none"]);
|
|
4
5
|
export const OperationConnectionSchema = z.object({
|
|
@@ -22,6 +23,10 @@ export const ErrorEnvelopeSchema = z.object({
|
|
|
22
23
|
message: z.string(),
|
|
23
24
|
requestId: z.string().optional(),
|
|
24
25
|
retryable: z.boolean(),
|
|
26
|
+
// Who stopped the request (honest-provider-error-contract); optional so
|
|
27
|
+
// older peers (stateful forwarding owners) remain parseable. Derived from
|
|
28
|
+
// the PROVIDER_ERROR_SOURCES registry so the enum cannot drift.
|
|
29
|
+
source: z.enum(PROVIDER_ERROR_SOURCES).optional(),
|
|
25
30
|
fix: z.string().optional(),
|
|
26
31
|
details: z.unknown().optional(),
|
|
27
32
|
});
|
package/dist/types.d.ts
CHANGED
|
@@ -610,7 +610,7 @@ export interface HealthMonitorProbeOverride {
|
|
|
610
610
|
/** Optional degraded threshold override for generated registry probes. */
|
|
611
611
|
degradedThresholdMs?: number;
|
|
612
612
|
}
|
|
613
|
-
export declare const VALID_OPERATION_ERROR_STATUSES: readonly [400, 401, 404, 429, 500, 502, 503, 504];
|
|
613
|
+
export declare const VALID_OPERATION_ERROR_STATUSES: readonly [400, 401, 404, 409, 410, 422, 429, 500, 502, 503, 504];
|
|
614
614
|
export type ProviderErrorStatus = (typeof VALID_OPERATION_ERROR_STATUSES)[number];
|
|
615
615
|
export interface OperationErrorCode {
|
|
616
616
|
code: string;
|
package/dist/types.js
CHANGED
|
@@ -32,7 +32,7 @@ export const HEALTH_CHECK_TIMEOUT_MS_MIN = 1;
|
|
|
32
32
|
export const HEALTH_CHECK_TIMEOUT_MS_MAX = 60_000;
|
|
33
33
|
export const HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MIN = 1;
|
|
34
34
|
export const HEALTH_CHECK_DEGRADED_THRESHOLD_MS_MAX = 60_000;
|
|
35
|
-
export const VALID_OPERATION_ERROR_STATUSES = [400, 401, 404, 429, 500, 502, 503, 504];
|
|
35
|
+
export const VALID_OPERATION_ERROR_STATUSES = [400, 401, 404, 409, 410, 422, 429, 500, 502, 503, 504];
|
|
36
36
|
export const HttpRetryPreset = {
|
|
37
37
|
Off: "off",
|
|
38
38
|
TransportTransient: "transport_transient",
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export {
|
|
|
39
39
|
export type { DevServerOptions } from "./dev.js";
|
|
40
40
|
export { createDevServer, startDevServer } from "./dev.js";
|
|
41
41
|
export * from "./errors.js";
|
|
42
|
+
export * from "./observability.js";
|
|
42
43
|
export * from "./user-input.js";
|
|
43
44
|
export * from "./i18n/index.js";
|
|
44
45
|
export {
|
package/src/observability.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// Mirrors packages/provider-observability in the platform monorepo (SoT).
|
|
2
|
+
export const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-08-07";
|
|
2
3
|
|
|
3
4
|
export const PROVIDER_ERROR_CATEGORIES = [
|
|
4
5
|
"ok",
|
|
@@ -7,6 +8,7 @@ export const PROVIDER_ERROR_CATEGORIES = [
|
|
|
7
8
|
"upstream_http",
|
|
8
9
|
"upstream_rate_limited",
|
|
9
10
|
"upstream_auth",
|
|
11
|
+
"upstream_rejected",
|
|
10
12
|
"upstream_schema_drift",
|
|
11
13
|
"proxy_pool",
|
|
12
14
|
"anti_bot_blocked",
|
|
@@ -16,6 +18,9 @@ export const PROVIDER_ERROR_CATEGORIES = [
|
|
|
16
18
|
"output_validation",
|
|
17
19
|
"provider_error",
|
|
18
20
|
"internal_error",
|
|
21
|
+
"dependency_unavailable",
|
|
22
|
+
"unsupported_transport",
|
|
23
|
+
"client_cancelled",
|
|
19
24
|
"unclassified",
|
|
20
25
|
] as const;
|
|
21
26
|
|
|
@@ -26,6 +31,9 @@ export function categoryForStatus(status: number): ProviderErrorCategory {
|
|
|
26
31
|
if (status === 408 || status === 504) return "timeout";
|
|
27
32
|
if (status === 429) return "upstream_rate_limited";
|
|
28
33
|
if (status === 401 || status === 403) return "upstream_auth";
|
|
34
|
+
// provider-error-contract: 409/410/422 are the deterministic
|
|
35
|
+
// upstream-rejection status class — not a retryable upstream failure.
|
|
36
|
+
if (status === 409 || status === 410 || status === 422) return "upstream_rejected";
|
|
29
37
|
if (status >= 400) return "upstream_http";
|
|
30
38
|
return "unclassified";
|
|
31
39
|
}
|
|
@@ -39,3 +47,45 @@ export function isRetryableCategory(category: ProviderErrorCategory): boolean {
|
|
|
39
47
|
category === "proxy_pool"
|
|
40
48
|
);
|
|
41
49
|
}
|
|
50
|
+
|
|
51
|
+
// Who stopped the request (honest-provider-error-contract). A deliberately
|
|
52
|
+
// small public projection of the internal taxonomy:
|
|
53
|
+
// - client: the request itself is fixable by the caller
|
|
54
|
+
// - upstream_rule: the upstream service refused it under its own business
|
|
55
|
+
// rules (deterministic) or rate limits
|
|
56
|
+
// - upstream_failure: the upstream service malfunctioned — retryable
|
|
57
|
+
// - apifuse: an APIFuse-side fault (provider bug, platform dependency)
|
|
58
|
+
export const PROVIDER_ERROR_SOURCES = [
|
|
59
|
+
"client",
|
|
60
|
+
"upstream_rule",
|
|
61
|
+
"upstream_failure",
|
|
62
|
+
"apifuse",
|
|
63
|
+
] as const;
|
|
64
|
+
|
|
65
|
+
export type ProviderErrorSource = (typeof PROVIDER_ERROR_SOURCES)[number];
|
|
66
|
+
|
|
67
|
+
export function sourceForCategory(category: ProviderErrorCategory): ProviderErrorSource {
|
|
68
|
+
switch (category) {
|
|
69
|
+
case "input_validation":
|
|
70
|
+
case "credential_expired":
|
|
71
|
+
case "credential_unavailable":
|
|
72
|
+
case "client_cancelled":
|
|
73
|
+
return "client";
|
|
74
|
+
case "upstream_rejected":
|
|
75
|
+
case "upstream_rate_limited":
|
|
76
|
+
return "upstream_rule";
|
|
77
|
+
case "timeout":
|
|
78
|
+
case "network":
|
|
79
|
+
case "upstream_http":
|
|
80
|
+
case "upstream_auth":
|
|
81
|
+
case "upstream_schema_drift":
|
|
82
|
+
case "anti_bot_blocked":
|
|
83
|
+
return "upstream_failure";
|
|
84
|
+
default:
|
|
85
|
+
// ok never reaches error serialization; provider_error,
|
|
86
|
+
// internal_error, output_validation, proxy_pool,
|
|
87
|
+
// dependency_unavailable, unsupported_transport, unclassified are
|
|
88
|
+
// APIFuse-side faults.
|
|
89
|
+
return "apifuse";
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/server/serve.ts
CHANGED
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
import type { ProviderLocale } from "../i18n/keys.js";
|
|
25
25
|
import {
|
|
26
26
|
categoryForStatus,
|
|
27
|
+
type ProviderErrorSource,
|
|
28
|
+
sourceForCategory,
|
|
27
29
|
isRetryableCategory,
|
|
28
30
|
PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
29
31
|
type ProviderErrorCategory,
|
|
@@ -541,6 +543,7 @@ export type ProviderServerLogEvent =
|
|
|
541
543
|
taxonomyVersion?: string;
|
|
542
544
|
retryable?: boolean;
|
|
543
545
|
signal?: "unregistered_provider_error_code";
|
|
546
|
+
signalFix?: string;
|
|
544
547
|
issues?: Array<{ path: string; code: string; message: string }>;
|
|
545
548
|
})
|
|
546
549
|
| {
|
|
@@ -658,12 +661,34 @@ function zodDetails(error: z.ZodError): Array<{
|
|
|
658
661
|
}));
|
|
659
662
|
}
|
|
660
663
|
|
|
664
|
+
// Category-level projection with code-aware honesty overrides: a missing
|
|
665
|
+
// deployment secret is an APIFuse-side defect even though its category
|
|
666
|
+
// (credential_unavailable) usually means a caller credential problem, an
|
|
667
|
+
// internal stateful-routing deadline is APIFuse-owned despite its timeout
|
|
668
|
+
// category, and the built-in upstream failure families keep their upstream
|
|
669
|
+
// attribution even when the author left the category at the provider_error
|
|
670
|
+
// default.
|
|
671
|
+
function publicErrorSource(
|
|
672
|
+
error: unknown,
|
|
673
|
+
category: ProviderErrorCategory,
|
|
674
|
+
): ProviderErrorSource {
|
|
675
|
+
if (error instanceof StatefulRoutingDeadlineError) return "apifuse";
|
|
676
|
+
if (isProviderError(error)) {
|
|
677
|
+
if (error.code === MISSING_SECRET_CODE) return "apifuse";
|
|
678
|
+
if (error.code === "UPSTREAM_ERROR" || error.code === "BLOCKED") {
|
|
679
|
+
return "upstream_failure";
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return sourceForCategory(category);
|
|
683
|
+
}
|
|
684
|
+
|
|
661
685
|
function toErrorResponse(
|
|
662
686
|
error: unknown,
|
|
663
687
|
requestId?: string,
|
|
664
688
|
declaredErrorCode?: OperationErrorCode,
|
|
665
689
|
): OperationErrorResponse {
|
|
666
690
|
const observability = errorObservabilityDetails(error, declaredErrorCode);
|
|
691
|
+
const source = publicErrorSource(error, observability.category);
|
|
667
692
|
if (error instanceof StatefulRoutingDeadlineError) {
|
|
668
693
|
return {
|
|
669
694
|
error: {
|
|
@@ -671,6 +696,7 @@ function toErrorResponse(
|
|
|
671
696
|
message: "Stateful forwarding deadline expired.",
|
|
672
697
|
...(requestId ? { requestId } : {}),
|
|
673
698
|
retryable: observability.retryable,
|
|
699
|
+
source,
|
|
674
700
|
},
|
|
675
701
|
};
|
|
676
702
|
}
|
|
@@ -683,6 +709,7 @@ function toErrorResponse(
|
|
|
683
709
|
message: publicProviderErrorMessage(error),
|
|
684
710
|
...(requestId ? { requestId } : {}),
|
|
685
711
|
retryable: observability.retryable,
|
|
712
|
+
source,
|
|
686
713
|
...(error.fix ? { fix: error.fix } : {}),
|
|
687
714
|
...(details !== undefined ? { details } : {}),
|
|
688
715
|
},
|
|
@@ -696,6 +723,7 @@ function toErrorResponse(
|
|
|
696
723
|
message: "Invalid request body",
|
|
697
724
|
...(requestId ? { requestId } : {}),
|
|
698
725
|
retryable: observability.retryable,
|
|
726
|
+
source,
|
|
699
727
|
details: zodDetails(error),
|
|
700
728
|
},
|
|
701
729
|
};
|
|
@@ -713,6 +741,7 @@ function toErrorResponse(
|
|
|
713
741
|
message: "Internal error",
|
|
714
742
|
...(requestId ? { requestId } : {}),
|
|
715
743
|
retryable: observability.retryable,
|
|
744
|
+
source,
|
|
716
745
|
details: {
|
|
717
746
|
retryable: false,
|
|
718
747
|
category: "internal_error",
|
|
@@ -804,9 +833,12 @@ function errorObservabilityDetails(
|
|
|
804
833
|
category:
|
|
805
834
|
isProviderError(error) && error.options?.category
|
|
806
835
|
? error.options.category
|
|
807
|
-
: isEmittableErrorStatus(declaredStatus) &&
|
|
808
|
-
|
|
809
|
-
|
|
836
|
+
: isEmittableErrorStatus(declaredStatus) &&
|
|
837
|
+
categoryForStatus(declaredStatus) === "upstream_rejected"
|
|
838
|
+
? "upstream_rejected"
|
|
839
|
+
: isEmittableErrorStatus(declaredStatus) && declaredStatus >= 500
|
|
840
|
+
? "provider_error"
|
|
841
|
+
: "input_validation",
|
|
810
842
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
811
843
|
retryable: isProviderError(error)
|
|
812
844
|
? (error.options?.retryable ?? effectiveDeclaration?.retryable ?? false)
|
|
@@ -823,8 +855,19 @@ function errorObservabilityDetails(
|
|
|
823
855
|
}
|
|
824
856
|
|
|
825
857
|
if (isProviderError(error)) {
|
|
858
|
+
// Deterministic upstream refusals default to the rejection category:
|
|
859
|
+
// the UPSTREAM_REJECTED family and any operation-declared rejection
|
|
860
|
+
// status (409/410/422) classify as upstream_rejected unless the
|
|
861
|
+
// author set an explicit category.
|
|
862
|
+
const declaredStatus = effectiveDeclaration?.status;
|
|
863
|
+
const rejectionDefault =
|
|
864
|
+
error.code === "UPSTREAM_REJECTED" ||
|
|
865
|
+
(isEmittableErrorStatus(declaredStatus) &&
|
|
866
|
+
categoryForStatus(declaredStatus) === "upstream_rejected")
|
|
867
|
+
? ("upstream_rejected" as const)
|
|
868
|
+
: ("provider_error" as const);
|
|
826
869
|
return {
|
|
827
|
-
category: error.options?.category ??
|
|
870
|
+
category: error.options?.category ?? rejectionDefault,
|
|
828
871
|
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
829
872
|
retryable: error.options?.retryable ?? effectiveDeclaration?.retryable ?? false,
|
|
830
873
|
};
|
|
@@ -915,6 +958,11 @@ function toStatusCode(error: unknown, declaredErrorCode?: OperationErrorCode): P
|
|
|
915
958
|
case "UPSTREAM_RATE_LIMIT":
|
|
916
959
|
case "LIMITED_NUMBER_OF_SERVICE_REQUESTS_EXCEEDS_ERROR":
|
|
917
960
|
return 429;
|
|
961
|
+
// Deterministic upstream business refusal (honest-provider-error-
|
|
962
|
+
// contract): the upstream evaluated the request and said no under
|
|
963
|
+
// its own rules — a conflict with upstream state, never a 5xx.
|
|
964
|
+
case "UPSTREAM_REJECTED":
|
|
965
|
+
return 409;
|
|
918
966
|
case "UPSTREAM_ERROR":
|
|
919
967
|
case "BLOCKED":
|
|
920
968
|
return 502;
|
|
@@ -1025,7 +1073,11 @@ function logProviderError(
|
|
|
1025
1073
|
taxonomyVersion: details.taxonomyVersion,
|
|
1026
1074
|
retryable: details.retryable,
|
|
1027
1075
|
...(isUnregisteredProviderErrorCode
|
|
1028
|
-
? {
|
|
1076
|
+
? {
|
|
1077
|
+
signal: "unregistered_provider_error_code" as const,
|
|
1078
|
+
signalFix:
|
|
1079
|
+
"Declare this code (with status and retryable) in the operation's docs.errorCodes so it serves its intended status instead of 500.",
|
|
1080
|
+
}
|
|
1029
1081
|
: {}),
|
|
1030
1082
|
...(error instanceof z.ZodError ? { issues: zodDetails(error) } : {}),
|
|
1031
1083
|
});
|
package/src/server/types.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { PROVIDER_ERROR_SOURCES } from "../observability.js";
|
|
2
3
|
|
|
3
4
|
import { HttpRetryPreset } from "../types.js";
|
|
4
5
|
|
|
@@ -27,6 +28,10 @@ export const ErrorEnvelopeSchema = z.object({
|
|
|
27
28
|
message: z.string(),
|
|
28
29
|
requestId: z.string().optional(),
|
|
29
30
|
retryable: z.boolean(),
|
|
31
|
+
// Who stopped the request (honest-provider-error-contract); optional so
|
|
32
|
+
// older peers (stateful forwarding owners) remain parseable. Derived from
|
|
33
|
+
// the PROVIDER_ERROR_SOURCES registry so the enum cannot drift.
|
|
34
|
+
source: z.enum(PROVIDER_ERROR_SOURCES).optional(),
|
|
30
35
|
fix: z.string().optional(),
|
|
31
36
|
details: z.unknown().optional(),
|
|
32
37
|
});
|
package/src/types.ts
CHANGED
|
@@ -719,7 +719,7 @@ export interface HealthMonitorProbeOverride {
|
|
|
719
719
|
degradedThresholdMs?: number;
|
|
720
720
|
}
|
|
721
721
|
|
|
722
|
-
export const VALID_OPERATION_ERROR_STATUSES = [400, 401, 404, 429, 500, 502, 503, 504] as const;
|
|
722
|
+
export const VALID_OPERATION_ERROR_STATUSES = [400, 401, 404, 409, 410, 422, 429, 500, 502, 503, 504] as const;
|
|
723
723
|
|
|
724
724
|
export type ProviderErrorStatus = (typeof VALID_OPERATION_ERROR_STATUSES)[number];
|
|
725
725
|
|