@slates/provider 1.0.0-rc.2 → 1.0.0-rc.21
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.cjs +2885 -2
- package/dist/index.d.cts +1019 -0
- package/dist/index.d.ts +1019 -9
- package/dist/index.module.js +2775 -2
- package/package.json +9 -6
- package/src/action/action.ts +92 -4
- package/src/action/attachment.ts +87 -0
- package/src/action/builder.test.ts +136 -0
- package/src/action/builder.ts +113 -15
- package/src/action/directUpload.test.ts +70 -0
- package/src/action/directUpload.ts +105 -0
- package/src/action/getFileUrlTool.ts +41 -0
- package/src/action/index.ts +4 -2
- package/src/action/publicTool.ts +36 -0
- package/src/action/scopes.test.ts +76 -0
- package/src/action/scopes.ts +103 -0
- package/src/action/tool.ts +22 -6
- package/src/action/trigger.ts +19 -11
- package/src/adapter/adapter.ts +38 -0
- package/src/adapter/index.test.ts +322 -0
- package/src/adapter/index.ts +2 -0
- package/src/adapter/spec.ts +154 -0
- package/src/auth/auth.ts +3 -3
- package/src/auth/index.ts +2 -0
- package/src/auth/oauth.test.ts +83 -0
- package/src/auth/oauth.ts +150 -0
- package/src/auth/redact.test.ts +103 -0
- package/src/auth/redact.ts +111 -0
- package/src/auth/types.ts +72 -13
- package/src/axios/http.test.ts +83 -0
- package/src/axios/index.ts +116 -10
- package/src/axios/trace.test.ts +377 -0
- package/src/axios/trace.ts +803 -0
- package/src/config/config.ts +14 -3
- package/src/context/context.ts +264 -17
- package/src/context/hook.test.ts +46 -0
- package/src/context/hook.ts +15 -6
- package/src/context/index.ts +1 -0
- package/src/context/pQueue.ts +4 -0
- package/src/crypto/hmac.test.ts +40 -0
- package/src/crypto/hmac.ts +32 -0
- package/src/crypto/index.ts +1 -0
- package/src/docs.ts +22 -0
- package/src/error/api.test.ts +114 -0
- package/src/error/api.ts +236 -0
- package/src/error/axios.ts +220 -0
- package/src/error/base.test.ts +178 -0
- package/src/error/base.ts +108 -2
- package/src/error/declaration.ts +7 -2
- package/src/error/defaults.ts +50 -0
- package/src/error/index.ts +4 -0
- package/src/error/service.ts +170 -0
- package/src/error/types.ts +75 -0
- package/src/error/utils.ts +157 -0
- package/src/file/index.test.ts +36 -0
- package/src/file/index.ts +27 -0
- package/src/index.ts +8 -0
- package/src/logger/logger.test.ts +57 -0
- package/src/logger/logger.ts +72 -11
- package/src/module/resolveDefaultExport.test.ts +32 -0
- package/src/module/resolveDefaultExport.ts +14 -0
- package/src/object/index.test.ts +38 -0
- package/src/object/index.ts +12 -0
- package/src/specification/slate.ts +40 -4
- package/src/specification/specification.ts +10 -6
- package/src/webhook/graph.test.ts +37 -0
- package/src/webhook/graph.ts +22 -0
- package/src/webhook/index.ts +2 -0
- package/src/webhook/meta.test.ts +52 -0
- package/src/webhook/meta.ts +43 -0
- package/dist/action/action.d.ts +0 -98
- package/dist/action/action.d.ts.map +0 -1
- package/dist/action/builder.d.ts +0 -27
- package/dist/action/builder.d.ts.map +0 -1
- package/dist/action/index.d.ts +0 -5
- package/dist/action/index.d.ts.map +0 -1
- package/dist/action/tool.d.ts +0 -14
- package/dist/action/tool.d.ts.map +0 -1
- package/dist/action/trigger.d.ts +0 -32
- package/dist/action/trigger.d.ts.map +0 -1
- package/dist/auth/auth.d.ts +0 -30
- package/dist/auth/auth.d.ts.map +0 -1
- package/dist/auth/index.d.ts +0 -3
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/types.d.ts +0 -148
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/axios/index.d.ts +0 -4
- package/dist/axios/index.d.ts.map +0 -1
- package/dist/config/config.d.ts +0 -26
- package/dist/config/config.d.ts.map +0 -1
- package/dist/config/index.d.ts +0 -2
- package/dist/config/index.d.ts.map +0 -1
- package/dist/context/context.d.ts +0 -20
- package/dist/context/context.d.ts.map +0 -1
- package/dist/context/hook.d.ts +0 -4
- package/dist/context/hook.d.ts.map +0 -1
- package/dist/context/index.d.ts +0 -2
- package/dist/context/index.d.ts.map +0 -1
- package/dist/error/base.d.ts +0 -5
- package/dist/error/base.d.ts.map +0 -1
- package/dist/error/declaration.d.ts +0 -6
- package/dist/error/declaration.d.ts.map +0 -1
- package/dist/error/index.d.ts +0 -3
- package/dist/error/index.d.ts.map +0 -1
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.modern.js +0 -2
- package/dist/index.modern.js.map +0 -1
- package/dist/index.module.js.map +0 -1
- package/dist/index.umd.js +0 -2
- package/dist/index.umd.js.map +0 -1
- package/dist/logger/index.d.ts +0 -2
- package/dist/logger/index.d.ts.map +0 -1
- package/dist/logger/logger.d.ts +0 -20
- package/dist/logger/logger.d.ts.map +0 -1
- package/dist/specification/index.d.ts +0 -3
- package/dist/specification/index.d.ts.map +0 -1
- package/dist/specification/slate.d.ts +0 -15
- package/dist/specification/slate.d.ts.map +0 -1
- package/dist/specification/specification.d.ts +0 -30
- package/dist/specification/specification.d.ts.map +0 -1
- package/dist/specification/zero.d.ts +0 -13
- package/dist/specification/zero.d.ts.map +0 -1
- package/dist/tokens.d.ts +0 -34
- package/dist/tokens.d.ts.map +0 -1
package/src/error/api.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { badRequestError, type ErrorRecord, ServiceError } from '@lowerdeck/error';
|
|
2
|
+
|
|
3
|
+
export type ApiErrorStatus = number | string;
|
|
4
|
+
export type ApiServiceError = ServiceError<ErrorRecord<any, any>>;
|
|
5
|
+
|
|
6
|
+
export type ApiErrorResponse = {
|
|
7
|
+
status?: ApiErrorStatus;
|
|
8
|
+
statusText?: string;
|
|
9
|
+
data?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ApiErrorDetailOptions = {
|
|
13
|
+
detailKeys?: readonly string[];
|
|
14
|
+
nestedKeys?: readonly string[];
|
|
15
|
+
includeNumbers?: boolean;
|
|
16
|
+
separator?: string;
|
|
17
|
+
fallbackMessage?: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type ApiErrorBuildHelpers = {
|
|
21
|
+
collectDetails: (value: unknown, details: string[], options?: ApiErrorDetailOptions) => void;
|
|
22
|
+
extractMessage: (error: unknown, options?: ApiErrorDetailOptions) => string;
|
|
23
|
+
getResponse: (error: unknown) => ApiErrorResponse | undefined;
|
|
24
|
+
getStatus: (error: unknown) => ApiErrorStatus | undefined;
|
|
25
|
+
isRecord: (value: unknown) => value is Record<string, unknown>;
|
|
26
|
+
statusLabelFor: (status?: ApiErrorStatus, statusText?: string) => string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type BuildApiServiceErrorOptions = ApiErrorDetailOptions & {
|
|
30
|
+
providerLabel: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
operation?: string;
|
|
33
|
+
extractMessage?: (error: unknown, helpers: ApiErrorBuildHelpers) => string | undefined;
|
|
34
|
+
extractResponse?: (
|
|
35
|
+
error: unknown,
|
|
36
|
+
helpers: ApiErrorBuildHelpers
|
|
37
|
+
) => ApiErrorResponse | undefined;
|
|
38
|
+
extractStatus?: (
|
|
39
|
+
error: unknown,
|
|
40
|
+
response: ApiErrorResponse | undefined,
|
|
41
|
+
helpers: ApiErrorBuildHelpers
|
|
42
|
+
) => ApiErrorStatus | undefined;
|
|
43
|
+
extractUpstreamCode?: (
|
|
44
|
+
error: unknown,
|
|
45
|
+
response: ApiErrorResponse | undefined,
|
|
46
|
+
helpers: ApiErrorBuildHelpers
|
|
47
|
+
) => string | undefined;
|
|
48
|
+
formatMessage?: (parts: {
|
|
49
|
+
providerLabel: string;
|
|
50
|
+
operation: string;
|
|
51
|
+
statusLabel: string;
|
|
52
|
+
message: string;
|
|
53
|
+
response?: ApiErrorResponse;
|
|
54
|
+
status?: ApiErrorStatus;
|
|
55
|
+
}) => string;
|
|
56
|
+
parent?: unknown;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
let DEFAULT_DETAIL_KEYS = ['message', 'detail', 'error', 'error_description', 'code'];
|
|
60
|
+
let DEFAULT_NESTED_KEYS = ['errors'];
|
|
61
|
+
|
|
62
|
+
export let isApiErrorRecord = (value: unknown): value is Record<string, unknown> =>
|
|
63
|
+
typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
64
|
+
|
|
65
|
+
let pushDetail = (
|
|
66
|
+
details: string[],
|
|
67
|
+
value: unknown,
|
|
68
|
+
options: Pick<ApiErrorDetailOptions, 'includeNumbers'> = {}
|
|
69
|
+
) => {
|
|
70
|
+
if (
|
|
71
|
+
typeof value !== 'string' &&
|
|
72
|
+
(options.includeNumbers === false || typeof value !== 'number')
|
|
73
|
+
) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let detail = String(value).trim();
|
|
78
|
+
if (detail && !details.includes(detail)) {
|
|
79
|
+
details.push(detail);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export let collectApiErrorDetails = (
|
|
84
|
+
value: unknown,
|
|
85
|
+
details: string[],
|
|
86
|
+
options: ApiErrorDetailOptions = {}
|
|
87
|
+
) => {
|
|
88
|
+
if (Array.isArray(value)) {
|
|
89
|
+
for (let item of value) {
|
|
90
|
+
collectApiErrorDetails(item, details, options);
|
|
91
|
+
}
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!isApiErrorRecord(value)) {
|
|
96
|
+
pushDetail(details, value, options);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (let key of options.detailKeys ?? DEFAULT_DETAIL_KEYS) {
|
|
101
|
+
pushDetail(details, value[key], options);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (let key of options.nestedKeys ?? DEFAULT_NESTED_KEYS) {
|
|
105
|
+
collectApiErrorDetails(value[key], details, options);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Runtime axios instances from createAxios reject with SlateError (the raw
|
|
110
|
+
// AxiosError is consumed by the response interceptor), so the upstream HTTP
|
|
111
|
+
// status only survives inside SlateError.data — read it back from there or
|
|
112
|
+
// every buildApiServiceError caller loses status-based handling in production.
|
|
113
|
+
let getSlateErrorResponse = (error: Record<string, unknown>): ApiErrorResponse | undefined => {
|
|
114
|
+
if (error.name !== 'SlateError' || !isApiErrorRecord(error.data)) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let data = error.data;
|
|
119
|
+
let upstream = isApiErrorRecord(data.upstream) ? data.upstream : undefined;
|
|
120
|
+
let status = upstream?.status ?? data.status;
|
|
121
|
+
return typeof status === 'number' || typeof status === 'string' ? { status } : undefined;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export let getApiErrorResponse = (error: unknown): ApiErrorResponse | undefined => {
|
|
125
|
+
if (!isApiErrorRecord(error)) {
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
if (!isApiErrorRecord(error.response)) {
|
|
129
|
+
return getSlateErrorResponse(error);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let response = error.response;
|
|
133
|
+
return {
|
|
134
|
+
status:
|
|
135
|
+
typeof response.status === 'number' || typeof response.status === 'string'
|
|
136
|
+
? response.status
|
|
137
|
+
: undefined,
|
|
138
|
+
statusText: typeof response.statusText === 'string' ? response.statusText : undefined,
|
|
139
|
+
data: response.data
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export let getApiErrorStatus = (error: unknown): ApiErrorStatus | undefined =>
|
|
144
|
+
getApiErrorResponse(error)?.status;
|
|
145
|
+
|
|
146
|
+
export let statusLabelForApiError = (status?: ApiErrorStatus, statusText?: string) =>
|
|
147
|
+
status !== undefined ? `HTTP ${status}${statusText ? ` ${statusText}` : ''}: ` : '';
|
|
148
|
+
|
|
149
|
+
export let extractApiErrorMessage = (
|
|
150
|
+
error: unknown,
|
|
151
|
+
options: ApiErrorDetailOptions & { response?: ApiErrorResponse } = {}
|
|
152
|
+
) => {
|
|
153
|
+
let details: string[] = [];
|
|
154
|
+
let response = options.response ?? getApiErrorResponse(error);
|
|
155
|
+
|
|
156
|
+
collectApiErrorDetails(response?.data, details, options);
|
|
157
|
+
|
|
158
|
+
if (details.length > 0) {
|
|
159
|
+
return details.join(options.separator ?? ' - ');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (error instanceof Error && error.message) {
|
|
163
|
+
return error.message;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return options.fallbackMessage ?? 'Unknown error';
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export let createApiServiceError = (
|
|
170
|
+
message: string,
|
|
171
|
+
options: {
|
|
172
|
+
reason?: string;
|
|
173
|
+
upstreamStatus?: ApiErrorStatus;
|
|
174
|
+
upstreamCode?: string;
|
|
175
|
+
parent?: unknown;
|
|
176
|
+
} = {}
|
|
177
|
+
) => {
|
|
178
|
+
let error = new ServiceError(badRequestError({ message }));
|
|
179
|
+
|
|
180
|
+
if (options.reason !== undefined) {
|
|
181
|
+
error.data.reason = options.reason;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if ('upstreamStatus' in options) {
|
|
185
|
+
error.data.upstreamStatus = options.upstreamStatus;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if ('upstreamCode' in options) {
|
|
189
|
+
error.data.upstreamCode = options.upstreamCode;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (options.parent instanceof Error) {
|
|
193
|
+
error.setParent(options.parent);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return error;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
let helpers: ApiErrorBuildHelpers = {
|
|
200
|
+
collectDetails: collectApiErrorDetails,
|
|
201
|
+
extractMessage: extractApiErrorMessage,
|
|
202
|
+
getResponse: getApiErrorResponse,
|
|
203
|
+
getStatus: getApiErrorStatus,
|
|
204
|
+
isRecord: isApiErrorRecord,
|
|
205
|
+
statusLabelFor: statusLabelForApiError
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export let buildApiServiceError = (error: unknown, options: BuildApiServiceErrorOptions) => {
|
|
209
|
+
if (error instanceof ServiceError) {
|
|
210
|
+
return error;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let operation = options.operation ?? 'request';
|
|
214
|
+
let response = options.extractResponse?.(error, helpers) ?? getApiErrorResponse(error);
|
|
215
|
+
let status = options.extractStatus?.(error, response, helpers) ?? response?.status;
|
|
216
|
+
let message =
|
|
217
|
+
options.extractMessage?.(error, helpers) ??
|
|
218
|
+
extractApiErrorMessage(error, { ...options, response });
|
|
219
|
+
let statusLabel = statusLabelForApiError(status, response?.statusText);
|
|
220
|
+
let fullMessage =
|
|
221
|
+
options.formatMessage?.({
|
|
222
|
+
providerLabel: options.providerLabel,
|
|
223
|
+
operation,
|
|
224
|
+
statusLabel,
|
|
225
|
+
message,
|
|
226
|
+
response,
|
|
227
|
+
status
|
|
228
|
+
}) ?? `${options.providerLabel} API ${operation} failed: ${statusLabel}${message}`;
|
|
229
|
+
|
|
230
|
+
return createApiServiceError(fullMessage, {
|
|
231
|
+
reason: options.reason,
|
|
232
|
+
upstreamStatus: status,
|
|
233
|
+
upstreamCode: options.extractUpstreamCode?.(error, response, helpers),
|
|
234
|
+
parent: options.parent ?? error
|
|
235
|
+
});
|
|
236
|
+
};
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import type { AxiosError } from 'axios';
|
|
2
|
+
import { isAxiosError } from 'axios';
|
|
3
|
+
import { getCurrentContext } from '../context/hook';
|
|
4
|
+
import type { SlateAxiosErrorOptions, SlateErrorResponse } from './types';
|
|
5
|
+
import {
|
|
6
|
+
getDefaultMessage,
|
|
7
|
+
isRecord,
|
|
8
|
+
mergeRecords,
|
|
9
|
+
normalizeKind,
|
|
10
|
+
normalizeRetryable,
|
|
11
|
+
normalizeSlateErrorInput,
|
|
12
|
+
sanitizeForBaggage
|
|
13
|
+
} from './utils';
|
|
14
|
+
|
|
15
|
+
let getResponseField = (value: unknown, key: string): unknown => {
|
|
16
|
+
if (!isRecord(value)) return undefined;
|
|
17
|
+
return value[key];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
let getNestedMessage = (value: unknown): string | undefined => {
|
|
21
|
+
if (typeof value === 'string') return value;
|
|
22
|
+
if (!isRecord(value)) return undefined;
|
|
23
|
+
|
|
24
|
+
let message = getResponseField(value, 'message');
|
|
25
|
+
if (typeof message === 'string' && message.trim()) return message;
|
|
26
|
+
|
|
27
|
+
let description = getResponseField(value, 'description');
|
|
28
|
+
if (typeof description === 'string' && description.trim()) return description;
|
|
29
|
+
|
|
30
|
+
let error = getResponseField(value, 'error');
|
|
31
|
+
if (typeof error === 'string' && error.trim()) return error;
|
|
32
|
+
if (isRecord(error)) {
|
|
33
|
+
let nested = getNestedMessage(error);
|
|
34
|
+
if (nested) return nested;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let errorDescription = getResponseField(value, 'error_description');
|
|
38
|
+
if (typeof errorDescription === 'string' && errorDescription.trim()) {
|
|
39
|
+
return errorDescription;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
let getNestedCode = (value: unknown): string | undefined => {
|
|
46
|
+
if (!isRecord(value)) return undefined;
|
|
47
|
+
|
|
48
|
+
let directCode = getResponseField(value, 'code');
|
|
49
|
+
if (typeof directCode === 'string' && directCode.trim()) return directCode;
|
|
50
|
+
|
|
51
|
+
let errorCode = getResponseField(value, 'error_code');
|
|
52
|
+
if (typeof errorCode === 'string' && errorCode.trim()) return errorCode;
|
|
53
|
+
|
|
54
|
+
let type = getResponseField(value, 'type');
|
|
55
|
+
if (typeof type === 'string' && type.trim()) return type;
|
|
56
|
+
|
|
57
|
+
let error = getResponseField(value, 'error');
|
|
58
|
+
if (typeof error === 'string' && error.trim()) return error;
|
|
59
|
+
if (isRecord(error)) return getNestedCode(error);
|
|
60
|
+
|
|
61
|
+
return undefined;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
let getNestedType = (value: unknown): string | undefined => {
|
|
65
|
+
if (!isRecord(value)) return undefined;
|
|
66
|
+
|
|
67
|
+
let type = getResponseField(value, 'type');
|
|
68
|
+
if (typeof type === 'string' && type.trim()) return type;
|
|
69
|
+
|
|
70
|
+
let error = getResponseField(value, 'error');
|
|
71
|
+
if (isRecord(error)) {
|
|
72
|
+
let nestedType = getNestedType(error);
|
|
73
|
+
if (nestedType) return nestedType;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return undefined;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
let getHeaderValue = (headers: unknown, key: string): string | undefined => {
|
|
80
|
+
if (!headers) return undefined;
|
|
81
|
+
|
|
82
|
+
if (typeof (headers as { get?: unknown }).get === 'function') {
|
|
83
|
+
let value = (headers as { get(name: string): unknown }).get(key);
|
|
84
|
+
if (typeof value === 'string' && value.trim()) return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (isRecord(headers)) {
|
|
88
|
+
for (let [headerKey, headerValue] of Object.entries(headers)) {
|
|
89
|
+
if (headerKey.toLowerCase() !== key.toLowerCase()) continue;
|
|
90
|
+
if (typeof headerValue === 'string' && headerValue.trim()) return headerValue;
|
|
91
|
+
if (Array.isArray(headerValue)) {
|
|
92
|
+
let first = headerValue.find(item => typeof item === 'string' && item.trim());
|
|
93
|
+
if (typeof first === 'string') return first;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return undefined;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
let buildRequestUrl = (error: AxiosError) => {
|
|
102
|
+
let baseURL = error.config?.baseURL;
|
|
103
|
+
let url = error.config?.url;
|
|
104
|
+
|
|
105
|
+
if (!url) return undefined;
|
|
106
|
+
if (!baseURL) return url;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
return new URL(url, baseURL).toString();
|
|
110
|
+
} catch {
|
|
111
|
+
return url;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
let getProviderDefaults = (): Partial<SlateErrorResponse> => {
|
|
116
|
+
try {
|
|
117
|
+
let ctx = getCurrentContext();
|
|
118
|
+
return {
|
|
119
|
+
provider: {
|
|
120
|
+
key: ctx.specification.key
|
|
121
|
+
},
|
|
122
|
+
...(ctx.getHttpTraces().length > 0 ? { requestTraces: ctx.getHttpTraces() } : {})
|
|
123
|
+
};
|
|
124
|
+
} catch {
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let inferAxiosCode = (error: AxiosError, data: unknown) => {
|
|
130
|
+
let status = error.response?.status;
|
|
131
|
+
let axiosCode = error.code?.toUpperCase();
|
|
132
|
+
let remoteCode = getNestedCode(data)?.toLowerCase();
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
axiosCode === 'ECONNABORTED' ||
|
|
136
|
+
axiosCode === 'ETIMEDOUT' ||
|
|
137
|
+
status === 408 ||
|
|
138
|
+
remoteCode?.includes('timeout')
|
|
139
|
+
) {
|
|
140
|
+
return 'upstream.timeout';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!error.response) {
|
|
144
|
+
return 'upstream.network_error';
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (status === 401) return 'auth.invalid';
|
|
148
|
+
if (status === 403) {
|
|
149
|
+
return remoteCode?.includes('rate') ? 'upstream.rate_limited' : 'permission.denied';
|
|
150
|
+
}
|
|
151
|
+
if (status === 404) return 'resource.not_found';
|
|
152
|
+
if (status === 409) return 'resource.conflict';
|
|
153
|
+
if (status === 422 || status === 400) return 'upstream.invalid_request';
|
|
154
|
+
if (status === 429) return 'upstream.rate_limited';
|
|
155
|
+
if (status === 502 || status === 503 || status === 504) return 'upstream.unavailable';
|
|
156
|
+
if (status && status >= 500) return 'upstream.error';
|
|
157
|
+
|
|
158
|
+
return 'upstream.error';
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
let inferAxiosMessage = (error: AxiosError, data: unknown, code: string) => {
|
|
162
|
+
return (
|
|
163
|
+
getNestedMessage(data) ??
|
|
164
|
+
error.response?.statusText ??
|
|
165
|
+
error.message ??
|
|
166
|
+
getDefaultMessage(code)
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let inferAxiosRetryable = (error: AxiosError, code: string) => {
|
|
171
|
+
let headerValue = getHeaderValue(error.response?.headers, 'retry-after');
|
|
172
|
+
if (headerValue) return true;
|
|
173
|
+
return normalizeRetryable(code, undefined);
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
export let isAxiosErrorLike = (error: unknown): error is AxiosError => isAxiosError(error);
|
|
177
|
+
|
|
178
|
+
export let inferSlateErrorFromAxios = (
|
|
179
|
+
error: AxiosError,
|
|
180
|
+
options: SlateAxiosErrorOptions = {}
|
|
181
|
+
): SlateErrorResponse => {
|
|
182
|
+
let responseData = error.response
|
|
183
|
+
? (options.extractResponseData?.(error.response) ?? error.response.data)
|
|
184
|
+
: undefined;
|
|
185
|
+
|
|
186
|
+
let code = inferAxiosCode(error, responseData);
|
|
187
|
+
|
|
188
|
+
return normalizeSlateErrorInput({
|
|
189
|
+
...getProviderDefaults(),
|
|
190
|
+
...options.defaults,
|
|
191
|
+
code: options.defaults?.code ?? code,
|
|
192
|
+
message: options.defaults?.message ?? inferAxiosMessage(error, responseData, code),
|
|
193
|
+
kind: options.defaults?.kind ?? normalizeKind(options.defaults?.code ?? code),
|
|
194
|
+
retryable:
|
|
195
|
+
options.defaults?.retryable ??
|
|
196
|
+
inferAxiosRetryable(error, options.defaults?.code ?? code),
|
|
197
|
+
status: options.defaults?.status ?? error.response?.status,
|
|
198
|
+
provider: {
|
|
199
|
+
...getProviderDefaults().provider,
|
|
200
|
+
...(options.defaults?.provider ?? {})
|
|
201
|
+
},
|
|
202
|
+
upstream: {
|
|
203
|
+
...(options.defaults?.upstream ?? {}),
|
|
204
|
+
status: error.response?.status ?? options.defaults?.upstream?.status,
|
|
205
|
+
code: getNestedCode(responseData) ?? options.defaults?.upstream?.code,
|
|
206
|
+
type: getNestedType(responseData) ?? options.defaults?.upstream?.type,
|
|
207
|
+
requestId:
|
|
208
|
+
getHeaderValue(error.response?.headers, 'x-request-id') ??
|
|
209
|
+
getHeaderValue(error.response?.headers, 'request-id') ??
|
|
210
|
+
getHeaderValue(error.response?.headers, 'x-correlation-id') ??
|
|
211
|
+
options.defaults?.upstream?.requestId,
|
|
212
|
+
method: error.config?.method?.toUpperCase() ?? options.defaults?.upstream?.method,
|
|
213
|
+
url: buildRequestUrl(error) ?? options.defaults?.upstream?.url
|
|
214
|
+
},
|
|
215
|
+
baggage: mergeRecords(options.defaults?.baggage, {
|
|
216
|
+
axiosCode: error.code,
|
|
217
|
+
response: sanitizeForBaggage(responseData)
|
|
218
|
+
})
|
|
219
|
+
});
|
|
220
|
+
};
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import {
|
|
2
|
+
badRequestError,
|
|
3
|
+
conflictError,
|
|
4
|
+
forbiddenError,
|
|
5
|
+
goneError,
|
|
6
|
+
internalServerError,
|
|
7
|
+
invalidVersion,
|
|
8
|
+
notAcceptableError,
|
|
9
|
+
notFoundError,
|
|
10
|
+
notImplementedError,
|
|
11
|
+
paymentRequiredError,
|
|
12
|
+
preconditionFailedError,
|
|
13
|
+
timeoutError,
|
|
14
|
+
tooManyRequestsError,
|
|
15
|
+
unauthorizedError,
|
|
16
|
+
validationError
|
|
17
|
+
} from '@lowerdeck/error';
|
|
18
|
+
import { AxiosError, type AxiosResponse, type InternalAxiosRequestConfig } from 'axios';
|
|
19
|
+
import { describe, expect, it } from 'vitest';
|
|
20
|
+
import { SlateError } from './base';
|
|
21
|
+
|
|
22
|
+
let createConfig = (overrides: Partial<InternalAxiosRequestConfig> = {}) =>
|
|
23
|
+
({
|
|
24
|
+
url: '/issues',
|
|
25
|
+
method: 'get',
|
|
26
|
+
headers: {},
|
|
27
|
+
...overrides
|
|
28
|
+
}) as InternalAxiosRequestConfig;
|
|
29
|
+
|
|
30
|
+
let createResponse = (overrides: Partial<AxiosResponse> = {}) =>
|
|
31
|
+
({
|
|
32
|
+
status: 429,
|
|
33
|
+
statusText: 'Too Many Requests',
|
|
34
|
+
headers: {
|
|
35
|
+
'x-request-id': 'req_123'
|
|
36
|
+
},
|
|
37
|
+
config: createConfig(),
|
|
38
|
+
data: {
|
|
39
|
+
error: {
|
|
40
|
+
code: 'rate_limited',
|
|
41
|
+
message: 'GitHub API rate limit exceeded',
|
|
42
|
+
type: 'rate_limit'
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
...overrides
|
|
46
|
+
}) as AxiosResponse;
|
|
47
|
+
|
|
48
|
+
describe('SlateError', () => {
|
|
49
|
+
it('maps axios responses to structured slate errors', () => {
|
|
50
|
+
let axiosError = new AxiosError(
|
|
51
|
+
'Request failed with status code 429',
|
|
52
|
+
'ERR_BAD_REQUEST',
|
|
53
|
+
createConfig(),
|
|
54
|
+
undefined,
|
|
55
|
+
createResponse()
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
let error = SlateError.fromAxios(axiosError, {
|
|
59
|
+
defaults: {
|
|
60
|
+
provider: {
|
|
61
|
+
service: 'github',
|
|
62
|
+
operation: 'issues.list'
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
expect(error).toBeInstanceOf(SlateError);
|
|
68
|
+
expect(error.code).toBe('upstream.rate_limited');
|
|
69
|
+
expect(error.status).toBe(429);
|
|
70
|
+
expect(error.retryable).toBe(true);
|
|
71
|
+
expect(error.data.provider).toEqual({
|
|
72
|
+
service: 'github',
|
|
73
|
+
operation: 'issues.list'
|
|
74
|
+
});
|
|
75
|
+
expect(error.data.upstream).toMatchObject({
|
|
76
|
+
status: 429,
|
|
77
|
+
code: 'rate_limited',
|
|
78
|
+
type: 'rate_limit',
|
|
79
|
+
requestId: 'req_123',
|
|
80
|
+
method: 'GET',
|
|
81
|
+
url: '/issues'
|
|
82
|
+
});
|
|
83
|
+
expect(error.data.baggage?.response).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('maps transport failures without responses', () => {
|
|
87
|
+
let axiosError = new AxiosError('socket hang up', 'ECONNRESET', createConfig());
|
|
88
|
+
|
|
89
|
+
let error = SlateError.fromAxios(axiosError);
|
|
90
|
+
|
|
91
|
+
expect(error.code).toBe('upstream.network_error');
|
|
92
|
+
expect(error.kind).toBe('upstream');
|
|
93
|
+
expect(error.retryable).toBe(true);
|
|
94
|
+
expect(error.data.upstream).toMatchObject({
|
|
95
|
+
method: 'GET',
|
|
96
|
+
url: '/issues'
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('lets providers override inferred axios mappings', () => {
|
|
101
|
+
let axiosError = new AxiosError(
|
|
102
|
+
'Request failed with status code 404',
|
|
103
|
+
'ERR_BAD_REQUEST',
|
|
104
|
+
createConfig({ method: 'post', url: '/contacts' }),
|
|
105
|
+
undefined,
|
|
106
|
+
createResponse({
|
|
107
|
+
status: 404,
|
|
108
|
+
statusText: 'Not Found',
|
|
109
|
+
data: {
|
|
110
|
+
error: {
|
|
111
|
+
code: 'contact_missing',
|
|
112
|
+
message: 'Contact does not exist'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
let error = SlateError.fromAxios(axiosError, {
|
|
119
|
+
mapAxiosError: (_, inferred) => ({
|
|
120
|
+
...inferred,
|
|
121
|
+
code: 'resource.not_found',
|
|
122
|
+
message: 'Salesforce contact was not found',
|
|
123
|
+
provider: {
|
|
124
|
+
...inferred.provider,
|
|
125
|
+
service: 'salesforce',
|
|
126
|
+
operation: 'contacts.create'
|
|
127
|
+
},
|
|
128
|
+
baggage: {
|
|
129
|
+
...inferred.baggage,
|
|
130
|
+
entity: 'contact'
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(error.code).toBe('resource.not_found');
|
|
136
|
+
expect(error.message).toBe('Salesforce contact was not found');
|
|
137
|
+
expect(error.data.provider).toEqual({
|
|
138
|
+
service: 'salesforce',
|
|
139
|
+
operation: 'contacts.create'
|
|
140
|
+
});
|
|
141
|
+
expect(error.data.baggage?.entity).toBe('contact');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('maps service errors to canonical slate errors', () => {
|
|
145
|
+
let serviceErrors = [
|
|
146
|
+
[
|
|
147
|
+
validationError({ entity: 'request', errors: [], message: 'Invalid request data' }),
|
|
148
|
+
'input.invalid'
|
|
149
|
+
],
|
|
150
|
+
[badRequestError({ message: 'Bad request' }), 'request.bad'],
|
|
151
|
+
[notFoundError('handler', 'missing'), 'resource.not_found'],
|
|
152
|
+
[unauthorizedError({ message: 'Please log in' }), 'auth.required'],
|
|
153
|
+
[forbiddenError({ message: 'Forbidden' }), 'permission.denied'],
|
|
154
|
+
[invalidVersion({ message: 'Invalid version' }), 'request.invalid_version'],
|
|
155
|
+
[conflictError({ message: 'Conflict' }), 'resource.conflict'],
|
|
156
|
+
[goneError({ message: 'Gone' }), 'resource.gone'],
|
|
157
|
+
[paymentRequiredError({ message: 'Payment required' }), 'payment.required'],
|
|
158
|
+
[
|
|
159
|
+
preconditionFailedError({ message: 'Precondition failed' }),
|
|
160
|
+
'request.precondition_failed'
|
|
161
|
+
],
|
|
162
|
+
[notAcceptableError({ message: 'Not acceptable' }), 'request.not_acceptable'],
|
|
163
|
+
[notImplementedError({ message: 'Not implemented' }), 'operation.not_implemented'],
|
|
164
|
+
[tooManyRequestsError({ message: 'Too many requests' }), 'request.rate_limited'],
|
|
165
|
+
[timeoutError({ message: 'Timed out' }), 'internal.timeout'],
|
|
166
|
+
[internalServerError({ message: 'Internal error' }), 'internal.unexpected']
|
|
167
|
+
] as const;
|
|
168
|
+
|
|
169
|
+
for (let [serviceError, code] of serviceErrors) {
|
|
170
|
+
let error = SlateError.fromServiceError(serviceError);
|
|
171
|
+
expect(error.code).toBe(code);
|
|
172
|
+
expect(error.data.baggage?.serviceError).toMatchObject({
|
|
173
|
+
code: serviceError.data.code,
|
|
174
|
+
status: serviceError.data.status
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|