@jskit-ai/http-runtime 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.descriptor.mjs +83 -0
- package/package.json +24 -0
- package/src/client/index.js +14 -0
- package/src/client/providers/HttpClientRuntimeClientProvider.js +21 -0
- package/src/client/providers/HttpValidatorsClientProvider.js +14 -0
- package/src/client/validationErrors.js +23 -0
- package/src/server/providers/HttpClientRuntimeServiceProvider.js +21 -0
- package/src/server/providers/HttpValidatorsServiceProvider.js +14 -0
- package/src/shared/clientRuntime/client.js +632 -0
- package/src/shared/clientRuntime/errors.js +35 -0
- package/src/shared/clientRuntime/headers.js +28 -0
- package/src/shared/clientRuntime/index.js +4 -0
- package/src/shared/clientRuntime/retry.js +38 -0
- package/src/shared/index.js +30 -0
- package/src/shared/providers/singletonApiProvider.js +27 -0
- package/src/shared/support/fieldErrors.js +31 -0
- package/src/shared/validators/command.js +34 -0
- package/src/shared/validators/errorResponses.js +108 -0
- package/src/shared/validators/httpValidatorsApi.js +58 -0
- package/src/shared/validators/operationMessages.js +149 -0
- package/src/shared/validators/operationValidation.js +126 -0
- package/src/shared/validators/paginationQuery.js +32 -0
- package/src/shared/validators/resource.js +43 -0
- package/src/shared/validators/schemaUtils.js +9 -0
- package/src/shared/validators/typeboxFormats.js +43 -0
- package/test/client.test.js +246 -0
- package/test/command.test.js +49 -0
- package/test/entrypoints.boundary.test.js +36 -0
- package/test/errorResponses.test.js +84 -0
- package/test/operationMessages.test.js +93 -0
- package/test/operationValidation.test.js +137 -0
- package/test/paginationQuery.test.js +32 -0
- package/test/providerRuntime.httpClient.test.js +35 -0
- package/test/providerRuntime.validators.test.js +39 -0
- package/test/resource.test.js +94 -0
- package/test/retry.test.js +41 -0
- package/test/typeboxFormats.test.js +42 -0
- package/test/validationErrors.test.js +100 -0
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
import { createHttpError, createNetworkError } from "./errors.js";
|
|
2
|
+
import { hasHeader, setHeaderIfMissing } from "./headers.js";
|
|
3
|
+
import { DEFAULT_RETRYABLE_CSRF_ERROR_CODES, shouldRetryForCsrfFailure } from "./retry.js";
|
|
4
|
+
|
|
5
|
+
const DEFAULT_UNSAFE_METHODS = Object.freeze(["POST", "PUT", "PATCH", "DELETE"]);
|
|
6
|
+
const DEFAULT_NDJSON_CONTENT_TYPE = "application/x-ndjson";
|
|
7
|
+
|
|
8
|
+
function normalizeMethod(method) {
|
|
9
|
+
return String(method || "GET")
|
|
10
|
+
.trim()
|
|
11
|
+
.toUpperCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function resolveUnsafeMethods(value) {
|
|
15
|
+
const source = Array.isArray(value) ? value : DEFAULT_UNSAFE_METHODS;
|
|
16
|
+
return new Set(source.map((method) => normalizeMethod(method)).filter(Boolean));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveFetch() {
|
|
20
|
+
if (typeof fetch === "function") {
|
|
21
|
+
return fetch;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
throw new Error("createHttpClient requires fetchImpl when global fetch is unavailable.");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isObjectBody(value) {
|
|
28
|
+
return Boolean(value) && typeof value === "object" && !(value instanceof FormData);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseJsonSafely(response) {
|
|
32
|
+
const contentType = String(response?.headers?.get?.("content-type") || "");
|
|
33
|
+
const isJson = contentType.includes("application/json");
|
|
34
|
+
if (!isJson) {
|
|
35
|
+
return Promise.resolve({
|
|
36
|
+
contentType,
|
|
37
|
+
isJson,
|
|
38
|
+
data: {}
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return Promise.resolve(response?.json?.())
|
|
43
|
+
.catch(() => ({}))
|
|
44
|
+
.then((data) => ({
|
|
45
|
+
contentType,
|
|
46
|
+
isJson,
|
|
47
|
+
data: data && typeof data === "object" ? data : {}
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function emitNdjsonLine(line, handlers) {
|
|
52
|
+
const normalizedLine = String(line || "").trim();
|
|
53
|
+
if (!normalizedLine) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const payload = JSON.parse(normalizedLine);
|
|
59
|
+
if (typeof handlers?.onEvent === "function") {
|
|
60
|
+
handlers.onEvent(payload);
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (typeof handlers?.onMalformedLine === "function") {
|
|
64
|
+
handlers.onMalformedLine(normalizedLine, error);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function readNdjsonStream(response, handlers = {}) {
|
|
70
|
+
if (!response?.body || typeof response.body.getReader !== "function") {
|
|
71
|
+
if (typeof response?.text === "function") {
|
|
72
|
+
const rawText = await response.text().catch(() => "");
|
|
73
|
+
for (const line of String(rawText || "").split(/\r?\n/g)) {
|
|
74
|
+
emitNdjsonLine(line, handlers);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const reader = response.body.getReader();
|
|
81
|
+
const decoder = new TextDecoder();
|
|
82
|
+
let buffered = "";
|
|
83
|
+
|
|
84
|
+
while (true) {
|
|
85
|
+
const { value, done } = await reader.read();
|
|
86
|
+
if (done) {
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
buffered += decoder.decode(value, {
|
|
91
|
+
stream: true
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const lines = buffered.split(/\r?\n/g);
|
|
95
|
+
buffered = lines.pop() || "";
|
|
96
|
+
for (const line of lines) {
|
|
97
|
+
emitNdjsonLine(line, handlers);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
buffered += decoder.decode();
|
|
102
|
+
if (buffered) {
|
|
103
|
+
emitNdjsonLine(buffered, handlers);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function createHttpClient(options = {}) {
|
|
108
|
+
const configuredFetchImpl = typeof options.fetchImpl === "function" ? options.fetchImpl : null;
|
|
109
|
+
const unsafeMethods = resolveUnsafeMethods(options.unsafeMethods);
|
|
110
|
+
const hooks = options?.hooks && typeof options.hooks === "object" ? options.hooks : {};
|
|
111
|
+
|
|
112
|
+
const csrf = {
|
|
113
|
+
enabled: options?.csrf?.enabled !== false,
|
|
114
|
+
sessionPath: String(options?.csrf?.sessionPath || "/api/session"),
|
|
115
|
+
headerName: String(options?.csrf?.headerName || "csrf-token"),
|
|
116
|
+
tokenField: String(options?.csrf?.tokenField || "csrfToken"),
|
|
117
|
+
retryableErrorCodes: Array.isArray(options?.csrf?.retryableErrorCodes)
|
|
118
|
+
? options.csrf.retryableErrorCodes
|
|
119
|
+
: DEFAULT_RETRYABLE_CSRF_ERROR_CODES
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
let csrfTokenCache = "";
|
|
123
|
+
let csrfFetchPromise = null;
|
|
124
|
+
|
|
125
|
+
function updateCsrfTokenFromPayload(data) {
|
|
126
|
+
const token = String(data?.[csrf.tokenField] || "");
|
|
127
|
+
if (token) {
|
|
128
|
+
csrfTokenCache = token;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function fetchSessionForCsrf() {
|
|
133
|
+
const activeFetch = configuredFetchImpl || resolveFetch();
|
|
134
|
+
let response;
|
|
135
|
+
try {
|
|
136
|
+
response = await activeFetch(csrf.sessionPath, {
|
|
137
|
+
method: "GET",
|
|
138
|
+
credentials: String(options?.credentials || "same-origin")
|
|
139
|
+
});
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
throw createNetworkError(cause);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const { data } = await parseJsonSafely(response);
|
|
145
|
+
updateCsrfTokenFromPayload(data);
|
|
146
|
+
|
|
147
|
+
if (!response.ok) {
|
|
148
|
+
throw createHttpError(response, data);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return data;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function ensureCsrfToken(forceRefresh = false) {
|
|
155
|
+
if (!csrf.enabled) {
|
|
156
|
+
return "";
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (!forceRefresh && csrfTokenCache) {
|
|
160
|
+
return csrfTokenCache;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!csrfFetchPromise || forceRefresh) {
|
|
164
|
+
csrfFetchPromise = fetchSessionForCsrf().finally(() => {
|
|
165
|
+
csrfFetchPromise = null;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await csrfFetchPromise;
|
|
170
|
+
return csrfTokenCache;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function decorateHeaders({ url, method, headers, requestOptions, state, stream }) {
|
|
174
|
+
if (typeof hooks.decorateHeaders !== "function") {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return hooks.decorateHeaders({
|
|
179
|
+
url,
|
|
180
|
+
method,
|
|
181
|
+
headers,
|
|
182
|
+
requestOptions,
|
|
183
|
+
state,
|
|
184
|
+
stream
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function notifyFailure(payload) {
|
|
189
|
+
if (typeof hooks.onFailure === "function") {
|
|
190
|
+
await hooks.onFailure(payload);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function notifySuccess(payload) {
|
|
195
|
+
if (typeof hooks.onSuccess === "function") {
|
|
196
|
+
await hooks.onSuccess(payload);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function maybeRetry({ response, method, state, data, stream }) {
|
|
201
|
+
if (!csrf.enabled) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const customShouldRetry =
|
|
206
|
+
typeof hooks.shouldRetryRequest === "function"
|
|
207
|
+
? await hooks.shouldRetryRequest({
|
|
208
|
+
response,
|
|
209
|
+
method,
|
|
210
|
+
state,
|
|
211
|
+
data,
|
|
212
|
+
stream
|
|
213
|
+
})
|
|
214
|
+
: null;
|
|
215
|
+
const shouldRetry =
|
|
216
|
+
customShouldRetry == null
|
|
217
|
+
? shouldRetryForCsrfFailure({
|
|
218
|
+
response,
|
|
219
|
+
method,
|
|
220
|
+
state,
|
|
221
|
+
data,
|
|
222
|
+
unsafeMethods,
|
|
223
|
+
retryableErrorCodes: csrf.retryableErrorCodes
|
|
224
|
+
})
|
|
225
|
+
: Boolean(customShouldRetry);
|
|
226
|
+
if (!shouldRetry) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (typeof hooks.onRetryableFailure === "function") {
|
|
231
|
+
await hooks.onRetryableFailure({
|
|
232
|
+
response,
|
|
233
|
+
method,
|
|
234
|
+
state,
|
|
235
|
+
data,
|
|
236
|
+
stream
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
csrfTokenCache = "";
|
|
241
|
+
await ensureCsrfToken(true);
|
|
242
|
+
state.csrfRetried = true;
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function resolveRequestState(state) {
|
|
247
|
+
const resolvedState = state && typeof state === "object" ? state : {};
|
|
248
|
+
if (typeof resolvedState.csrfRetried !== "boolean") {
|
|
249
|
+
resolvedState.csrfRetried = false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return resolvedState;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function prepareRequestConfig(url, requestOptions, state, stream) {
|
|
256
|
+
const resolvedState = resolveRequestState(state);
|
|
257
|
+
|
|
258
|
+
const method = normalizeMethod(requestOptions.method);
|
|
259
|
+
const headers =
|
|
260
|
+
requestOptions.headers && typeof requestOptions.headers === "object" ? { ...requestOptions.headers } : {};
|
|
261
|
+
|
|
262
|
+
const decorateHeadersResult = decorateHeaders({
|
|
263
|
+
url,
|
|
264
|
+
method,
|
|
265
|
+
headers,
|
|
266
|
+
requestOptions,
|
|
267
|
+
state: resolvedState,
|
|
268
|
+
stream: Boolean(stream)
|
|
269
|
+
});
|
|
270
|
+
if (decorateHeadersResult && typeof decorateHeadersResult.then === "function") {
|
|
271
|
+
await decorateHeadersResult;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const config = {
|
|
275
|
+
credentials: String(options?.credentials || "same-origin"),
|
|
276
|
+
...requestOptions,
|
|
277
|
+
method,
|
|
278
|
+
headers
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
if (isObjectBody(config.body)) {
|
|
282
|
+
setHeaderIfMissing(headers, "Content-Type", "application/json");
|
|
283
|
+
config.body = JSON.stringify(config.body);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (csrf.enabled && unsafeMethods.has(method) && !hasHeader(headers, csrf.headerName)) {
|
|
287
|
+
const token = await ensureCsrfToken();
|
|
288
|
+
if (token) {
|
|
289
|
+
setHeaderIfMissing(headers, csrf.headerName, token);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
method,
|
|
295
|
+
config,
|
|
296
|
+
state: resolvedState
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function executePreparedRequest(url, config, { method, state }, onNetworkFailure) {
|
|
301
|
+
let response;
|
|
302
|
+
try {
|
|
303
|
+
const activeFetch = configuredFetchImpl || resolveFetch();
|
|
304
|
+
response = await activeFetch(url, config);
|
|
305
|
+
} catch (cause) {
|
|
306
|
+
return onNetworkFailure(cause);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const { contentType, isJson, data } = await parseJsonSafely(response);
|
|
310
|
+
updateCsrfTokenFromPayload(data);
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
response,
|
|
314
|
+
data,
|
|
315
|
+
contentType,
|
|
316
|
+
isJson,
|
|
317
|
+
method,
|
|
318
|
+
state
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function handleHttpFailure({
|
|
323
|
+
url,
|
|
324
|
+
method,
|
|
325
|
+
state,
|
|
326
|
+
response,
|
|
327
|
+
data,
|
|
328
|
+
contentType,
|
|
329
|
+
isJson,
|
|
330
|
+
stream,
|
|
331
|
+
retryRequest
|
|
332
|
+
}) {
|
|
333
|
+
if (
|
|
334
|
+
await maybeRetry({
|
|
335
|
+
response,
|
|
336
|
+
method,
|
|
337
|
+
state,
|
|
338
|
+
data,
|
|
339
|
+
stream
|
|
340
|
+
})
|
|
341
|
+
) {
|
|
342
|
+
return retryRequest();
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const error = createHttpError(response, data);
|
|
346
|
+
if (Number(response.status) === 401 && typeof hooks.onUnauthorized === "function") {
|
|
347
|
+
await hooks.onUnauthorized(error);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
await notifyFailure({
|
|
351
|
+
url,
|
|
352
|
+
method,
|
|
353
|
+
state,
|
|
354
|
+
reason: `http_${response.status}`,
|
|
355
|
+
error,
|
|
356
|
+
response,
|
|
357
|
+
data,
|
|
358
|
+
contentType,
|
|
359
|
+
isJson,
|
|
360
|
+
stream
|
|
361
|
+
});
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function executeRequestLifecycle({
|
|
366
|
+
url,
|
|
367
|
+
requestOptions = {},
|
|
368
|
+
state = null,
|
|
369
|
+
stream = false,
|
|
370
|
+
handleNetworkFailure,
|
|
371
|
+
retryRequest
|
|
372
|
+
} = {}) {
|
|
373
|
+
if (typeof handleNetworkFailure !== "function") {
|
|
374
|
+
throw new TypeError("executeRequestLifecycle requires handleNetworkFailure().");
|
|
375
|
+
}
|
|
376
|
+
if (typeof retryRequest !== "function") {
|
|
377
|
+
throw new TypeError("executeRequestLifecycle requires retryRequest().");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const requestContext = await prepareRequestConfig(url, requestOptions, state, stream);
|
|
381
|
+
const {
|
|
382
|
+
method,
|
|
383
|
+
state: resolvedState
|
|
384
|
+
} = requestContext;
|
|
385
|
+
const result = await executePreparedRequest(
|
|
386
|
+
url,
|
|
387
|
+
requestContext.config,
|
|
388
|
+
requestContext,
|
|
389
|
+
(cause) =>
|
|
390
|
+
handleNetworkFailure({
|
|
391
|
+
cause,
|
|
392
|
+
url,
|
|
393
|
+
method,
|
|
394
|
+
state: resolvedState,
|
|
395
|
+
stream
|
|
396
|
+
})
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
if (!result.response.ok) {
|
|
400
|
+
return {
|
|
401
|
+
handled: true,
|
|
402
|
+
value: await handleHttpFailure({
|
|
403
|
+
url,
|
|
404
|
+
method,
|
|
405
|
+
state: resolvedState,
|
|
406
|
+
response: result.response,
|
|
407
|
+
data: result.data,
|
|
408
|
+
contentType: result.contentType,
|
|
409
|
+
isJson: result.isJson,
|
|
410
|
+
stream,
|
|
411
|
+
retryRequest() {
|
|
412
|
+
return retryRequest(resolvedState);
|
|
413
|
+
}
|
|
414
|
+
})
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return {
|
|
419
|
+
handled: false,
|
|
420
|
+
value: {
|
|
421
|
+
method,
|
|
422
|
+
state: resolvedState,
|
|
423
|
+
result
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function request(url, requestOptions = {}, state = null) {
|
|
429
|
+
const execution = await executeRequestLifecycle({
|
|
430
|
+
url,
|
|
431
|
+
requestOptions,
|
|
432
|
+
state,
|
|
433
|
+
stream: false,
|
|
434
|
+
async handleNetworkFailure({ cause, method, state: resolvedState }) {
|
|
435
|
+
const error = createNetworkError(cause);
|
|
436
|
+
await notifyFailure({
|
|
437
|
+
url,
|
|
438
|
+
method,
|
|
439
|
+
state: resolvedState,
|
|
440
|
+
reason: "network_error",
|
|
441
|
+
error,
|
|
442
|
+
stream: false
|
|
443
|
+
});
|
|
444
|
+
throw error;
|
|
445
|
+
},
|
|
446
|
+
retryRequest(nextState) {
|
|
447
|
+
return request(url, requestOptions, nextState);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
if (execution.handled) {
|
|
451
|
+
return execution.value;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const {
|
|
455
|
+
method,
|
|
456
|
+
state: resolvedState,
|
|
457
|
+
result
|
|
458
|
+
} = execution.value;
|
|
459
|
+
|
|
460
|
+
await notifySuccess({
|
|
461
|
+
url,
|
|
462
|
+
method,
|
|
463
|
+
state: resolvedState,
|
|
464
|
+
response: result.response,
|
|
465
|
+
data: result.data,
|
|
466
|
+
contentType: result.contentType,
|
|
467
|
+
isJson: result.isJson,
|
|
468
|
+
stream: false
|
|
469
|
+
});
|
|
470
|
+
return result.data;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async function requestStream(url, requestOptions = {}, handlers = {}, state = null) {
|
|
474
|
+
const execution = await executeRequestLifecycle({
|
|
475
|
+
url,
|
|
476
|
+
requestOptions,
|
|
477
|
+
state,
|
|
478
|
+
stream: true,
|
|
479
|
+
async handleNetworkFailure({ cause, method, state: resolvedState }) {
|
|
480
|
+
const aborted = String(cause?.name || "") === "AbortError";
|
|
481
|
+
const reason = aborted ? "aborted" : "network_error";
|
|
482
|
+
await notifyFailure({
|
|
483
|
+
url,
|
|
484
|
+
method,
|
|
485
|
+
state: resolvedState,
|
|
486
|
+
reason,
|
|
487
|
+
error: cause,
|
|
488
|
+
stream: true
|
|
489
|
+
});
|
|
490
|
+
if (aborted) {
|
|
491
|
+
throw cause;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
throw createNetworkError(cause);
|
|
495
|
+
},
|
|
496
|
+
retryRequest(nextState) {
|
|
497
|
+
return requestStream(url, requestOptions, handlers, nextState);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
if (execution.handled) {
|
|
501
|
+
return execution.value;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const {
|
|
505
|
+
method,
|
|
506
|
+
state: resolvedState,
|
|
507
|
+
result
|
|
508
|
+
} = execution.value;
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
let shouldParseAsNdjson = result.contentType.includes(DEFAULT_NDJSON_CONTENT_TYPE);
|
|
512
|
+
if (!shouldParseAsNdjson && typeof hooks.shouldTreatAsNdjsonStream === "function") {
|
|
513
|
+
shouldParseAsNdjson = Boolean(
|
|
514
|
+
await hooks.shouldTreatAsNdjsonStream({
|
|
515
|
+
url,
|
|
516
|
+
method,
|
|
517
|
+
state: resolvedState,
|
|
518
|
+
contentType: result.contentType,
|
|
519
|
+
isJson: result.isJson,
|
|
520
|
+
data: result.data,
|
|
521
|
+
response: result.response
|
|
522
|
+
})
|
|
523
|
+
);
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (shouldParseAsNdjson) {
|
|
527
|
+
await readNdjsonStream(result.response, handlers);
|
|
528
|
+
} else if (typeof handlers?.onEvent === "function" && Object.keys(result.data).length > 0) {
|
|
529
|
+
handlers.onEvent(result.data);
|
|
530
|
+
} else if (typeof handlers?.onEvent === "function" && typeof result.response?.text === "function") {
|
|
531
|
+
const rawText = await result.response.text().catch(() => "");
|
|
532
|
+
for (const line of String(rawText || "").split(/\r?\n/g)) {
|
|
533
|
+
emitNdjsonLine(line, handlers);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} catch (error) {
|
|
537
|
+
await notifyFailure({
|
|
538
|
+
url,
|
|
539
|
+
method,
|
|
540
|
+
state: resolvedState,
|
|
541
|
+
reason: "stream_error",
|
|
542
|
+
error,
|
|
543
|
+
response: result.response,
|
|
544
|
+
data: result.data,
|
|
545
|
+
contentType: result.contentType,
|
|
546
|
+
isJson: result.isJson,
|
|
547
|
+
stream: true
|
|
548
|
+
});
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
await notifySuccess({
|
|
553
|
+
url,
|
|
554
|
+
method,
|
|
555
|
+
state: resolvedState,
|
|
556
|
+
response: result.response,
|
|
557
|
+
data: result.data,
|
|
558
|
+
contentType: result.contentType,
|
|
559
|
+
isJson: result.isJson,
|
|
560
|
+
stream: true
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function clearCsrfTokenCache() {
|
|
565
|
+
csrfTokenCache = "";
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function resetForTests() {
|
|
569
|
+
csrfTokenCache = "";
|
|
570
|
+
csrfFetchPromise = null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function get(url, requestOptions = {}) {
|
|
574
|
+
return request(url, {
|
|
575
|
+
...requestOptions,
|
|
576
|
+
method: "GET"
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function post(url, body, requestOptions = {}) {
|
|
581
|
+
return request(url, {
|
|
582
|
+
...requestOptions,
|
|
583
|
+
method: "POST",
|
|
584
|
+
body
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function put(url, body, requestOptions = {}) {
|
|
589
|
+
return request(url, {
|
|
590
|
+
...requestOptions,
|
|
591
|
+
method: "PUT",
|
|
592
|
+
body
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function patch(url, body, requestOptions = {}) {
|
|
597
|
+
return request(url, {
|
|
598
|
+
...requestOptions,
|
|
599
|
+
method: "PATCH",
|
|
600
|
+
body
|
|
601
|
+
});
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function del(url, requestOptions = {}) {
|
|
605
|
+
return request(url, {
|
|
606
|
+
...requestOptions,
|
|
607
|
+
method: "DELETE"
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return {
|
|
612
|
+
request,
|
|
613
|
+
requestStream,
|
|
614
|
+
ensureCsrfToken,
|
|
615
|
+
clearCsrfTokenCache,
|
|
616
|
+
resetForTests,
|
|
617
|
+
get,
|
|
618
|
+
post,
|
|
619
|
+
put,
|
|
620
|
+
patch,
|
|
621
|
+
delete: del,
|
|
622
|
+
__testables: {
|
|
623
|
+
fetchSessionForCsrf,
|
|
624
|
+
updateCsrfTokenFromPayload,
|
|
625
|
+
createHttpError,
|
|
626
|
+
createNetworkError,
|
|
627
|
+
readNdjsonStream
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
export { createHttpClient };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { isRecord, resolveFieldErrors } from "../support/fieldErrors.js";
|
|
2
|
+
|
|
3
|
+
function createHttpError(response, data = {}) {
|
|
4
|
+
const payload = isRecord(data) ? data : {};
|
|
5
|
+
const error = new Error(payload.error || `Request failed with status ${response.status}.`);
|
|
6
|
+
const normalizedFieldErrors = resolveFieldErrors(payload);
|
|
7
|
+
error.status = Number(response?.status || 0);
|
|
8
|
+
error.code = String(payload.code || "").trim() || null;
|
|
9
|
+
error.fieldErrors = Object.keys(normalizedFieldErrors).length > 0 ? normalizedFieldErrors : null;
|
|
10
|
+
if (isRecord(payload.details)) {
|
|
11
|
+
error.details = payload.details;
|
|
12
|
+
} else if (error.fieldErrors) {
|
|
13
|
+
error.details = {
|
|
14
|
+
fieldErrors: error.fieldErrors
|
|
15
|
+
};
|
|
16
|
+
} else {
|
|
17
|
+
error.details = null;
|
|
18
|
+
}
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createNetworkError(cause) {
|
|
23
|
+
const error = new Error("Network request failed.");
|
|
24
|
+
error.status = 0;
|
|
25
|
+
error.code = null;
|
|
26
|
+
error.fieldErrors = null;
|
|
27
|
+
error.details = null;
|
|
28
|
+
error.cause = cause;
|
|
29
|
+
return error;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
createHttpError,
|
|
34
|
+
createNetworkError
|
|
35
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function normalizeHeaderName(name) {
|
|
2
|
+
return String(name || "")
|
|
3
|
+
.trim()
|
|
4
|
+
.toLowerCase();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function hasHeader(headers, name) {
|
|
8
|
+
const normalizedTarget = normalizeHeaderName(name);
|
|
9
|
+
if (!normalizedTarget || !headers || typeof headers !== "object") {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return Object.keys(headers).some((key) => normalizeHeaderName(key) === normalizedTarget);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function setHeaderIfMissing(headers, name, value) {
|
|
17
|
+
if (!headers || typeof headers !== "object" || !name || value == null) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (hasHeader(headers, name)) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
headers[name] = value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { normalizeHeaderName, hasHeader, setHeaderIfMissing };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createHttpClient } from "./client.js";
|
|
2
|
+
export { createHttpError, createNetworkError } from "./errors.js";
|
|
3
|
+
export { DEFAULT_RETRYABLE_CSRF_ERROR_CODES, shouldRetryForCsrfFailure } from "./retry.js";
|
|
4
|
+
export { normalizeHeaderName, hasHeader, setHeaderIfMissing } from "./headers.js";
|