@outputai/http 0.2.1-next.af8a069.0 → 0.2.1-next.eadab44.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/fetch/index.d.ts +22 -0
- package/dist/fetch/index.js +45 -0
- package/dist/fetch/index.spec.js +206 -0
- package/dist/fetch/logger.d.ts +45 -0
- package/dist/fetch/logger.js +54 -0
- package/dist/fetch/logger.spec.js +182 -0
- package/dist/fetch/utils.d.ts +36 -0
- package/dist/fetch/utils.js +91 -0
- package/dist/fetch/utils.spec.js +253 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -26
- package/dist/index.spec.js +11 -389
- package/package.json +3 -2
- package/dist/hooks/assign_request_id.d.ts +0 -9
- package/dist/hooks/assign_request_id.js +0 -15
- package/dist/hooks/index.d.ts +0 -4
- package/dist/hooks/index.js +0 -4
- package/dist/hooks/trace_error.d.ts +0 -14
- package/dist/hooks/trace_error.js +0 -61
- package/dist/hooks/trace_error.spec.js +0 -35
- package/dist/hooks/trace_request.d.ts +0 -6
- package/dist/hooks/trace_request.js +0 -25
- package/dist/hooks/trace_request.spec.js +0 -60
- package/dist/hooks/trace_response.d.ts +0 -6
- package/dist/hooks/trace_response.js +0 -26
- package/dist/hooks/trace_response.spec.js +0 -68
- package/dist/index.integration.test.d.ts +0 -1
- package/dist/index.integration.test.js +0 -389
- package/dist/utils/create_trace_id.d.ts +0 -13
- package/dist/utils/create_trace_id.js +0 -20
- package/dist/utils/create_trace_id.spec.d.ts +0 -1
- package/dist/utils/create_trace_id.spec.js +0 -20
- package/dist/utils/index.d.ts +0 -4
- package/dist/utils/index.js +0 -4
- package/dist/utils/parse_request_body.d.ts +0 -7
- package/dist/utils/parse_request_body.js +0 -19
- package/dist/utils/parse_request_body.spec.d.ts +0 -1
- package/dist/utils/parse_request_body.spec.js +0 -19
- package/dist/utils/parse_response_body.d.ts +0 -10
- package/dist/utils/parse_response_body.js +0 -14
- package/dist/utils/parse_response_body.spec.d.ts +0 -1
- package/dist/utils/parse_response_body.spec.js +0 -19
- package/dist/utils/redact_headers.d.ts +0 -6
- package/dist/utils/redact_headers.js +0 -27
- package/dist/utils/redact_headers.spec.d.ts +0 -1
- package/dist/utils/redact_headers.spec.js +0 -245
- /package/dist/{hooks/trace_error.spec.d.ts → fetch/index.spec.d.ts} +0 -0
- /package/dist/{hooks/trace_request.spec.d.ts → fetch/logger.spec.d.ts} +0 -0
- /package/dist/{hooks/trace_response.spec.d.ts → fetch/utils.spec.d.ts} +0 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Header names that look sensitive by substring rules but are not secret material.
|
|
3
|
+
*/
|
|
4
|
+
const HEADER_REDACTION_EXEMPT = new Set([
|
|
5
|
+
'x-csrf-token',
|
|
6
|
+
'public-key-pins'
|
|
7
|
+
]);
|
|
8
|
+
/** Matches red int "hot-red-pie", but not int "redact" */
|
|
9
|
+
const wordMatcher = (term) => new RegExp(`(?<![a-z\\d])${term}(?![a-z\\d])`, 'i');
|
|
10
|
+
/** Matches red in "acquired", but not in "redact" */
|
|
11
|
+
const wordEndMatcher = (term) => new RegExp(`${term}(?![a-z\\d])`, 'i');
|
|
12
|
+
/**
|
|
13
|
+
* Sensitive header patterns for redaction (case-insensitive).
|
|
14
|
+
* Uses alphanumeric boundaries so e.g. `token` does not match inside `tokens`.
|
|
15
|
+
*/
|
|
16
|
+
const SENSITIVE_HEADER_PATTERNS = [
|
|
17
|
+
// matches headers that contain these exact words
|
|
18
|
+
wordMatcher('authorization'),
|
|
19
|
+
wordMatcher('token'),
|
|
20
|
+
wordMatcher('secret'),
|
|
21
|
+
wordMatcher('password'),
|
|
22
|
+
wordMatcher('pwd'),
|
|
23
|
+
wordMatcher('cookie'),
|
|
24
|
+
// matches header that contain words ending with these sequences
|
|
25
|
+
wordEndMatcher('key')
|
|
26
|
+
];
|
|
27
|
+
/**
|
|
28
|
+
* Serialize a given error to a plain object keeping main properties:
|
|
29
|
+
* - name (from constructor.name)
|
|
30
|
+
* - message
|
|
31
|
+
* - stack
|
|
32
|
+
* - code (optional, but present on Node errors)
|
|
33
|
+
* - cause (error chain)
|
|
34
|
+
*
|
|
35
|
+
* @param error Error to serialize
|
|
36
|
+
* @param depth Current recursion depth for the error.cause chain
|
|
37
|
+
* @returns Object
|
|
38
|
+
*/
|
|
39
|
+
export const serializeError = (error, depth = 1) => ({
|
|
40
|
+
name: error.constructor.name,
|
|
41
|
+
message: error.message,
|
|
42
|
+
stack: error.stack,
|
|
43
|
+
code: error.code ?? undefined,
|
|
44
|
+
cause: (() => {
|
|
45
|
+
if (depth > 5) {
|
|
46
|
+
return '<Max recursion depth reached>';
|
|
47
|
+
}
|
|
48
|
+
if (error.cause instanceof Error) {
|
|
49
|
+
return serializeError(error.cause, depth + 1);
|
|
50
|
+
}
|
|
51
|
+
return undefined; // eslint-disable-line consistent-return
|
|
52
|
+
})()
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Redacts sensitive headers for safe logging
|
|
56
|
+
*
|
|
57
|
+
* @param headers
|
|
58
|
+
* @returns Plain object with sensitive headers redacted
|
|
59
|
+
*/
|
|
60
|
+
export const redactHeaders = (headers) => {
|
|
61
|
+
const result = {};
|
|
62
|
+
for (const [key, value] of headers.entries()) {
|
|
63
|
+
const lowerCaseKey = key.toLowerCase();
|
|
64
|
+
const isSensitive = !HEADER_REDACTION_EXEMPT.has(lowerCaseKey) &&
|
|
65
|
+
SENSITIVE_HEADER_PATTERNS.some(pattern => pattern.test(key));
|
|
66
|
+
result[key] = isSensitive ? '[REDACTED]' : value;
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Clones a Request or Response object and reads the body as text, then:
|
|
72
|
+
* - non-JSON content-type, or empty body: returns the text as-is
|
|
73
|
+
* - application/json with a non-empty body: returns JSON.parse result, or the raw text if parsing fails
|
|
74
|
+
*
|
|
75
|
+
* @param r
|
|
76
|
+
* @returns Parsed JSON value or raw body string
|
|
77
|
+
*/
|
|
78
|
+
export const parseBody = async (r) => {
|
|
79
|
+
const clone = r.clone();
|
|
80
|
+
const contentType = clone.headers.get('content-type') || '';
|
|
81
|
+
const textContent = await clone.text();
|
|
82
|
+
if (!contentType.includes('application/json') || textContent.length === 0) {
|
|
83
|
+
return textContent;
|
|
84
|
+
}
|
|
85
|
+
try {
|
|
86
|
+
return JSON.parse(textContent);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return textContent;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Response, Request, Headers } from 'undici';
|
|
3
|
+
import { parseBody, redactHeaders, serializeError } from './utils.js';
|
|
4
|
+
const createMultiLevelError = (levels, depth = 1) => depth === levels ?
|
|
5
|
+
new Error(`level-${depth}`) :
|
|
6
|
+
new Error(`level-${depth}`, { cause: createMultiLevelError(levels, depth + 1) });
|
|
7
|
+
const walkSerializedCause = (root, steps) => {
|
|
8
|
+
if (steps === 0) {
|
|
9
|
+
return root;
|
|
10
|
+
}
|
|
11
|
+
return walkSerializedCause(root.cause, steps - 1);
|
|
12
|
+
};
|
|
13
|
+
describe('fetch/utils', () => {
|
|
14
|
+
describe('serializeError', () => {
|
|
15
|
+
it('serializes name, message, stack and sets code and cause to undefined when absent', () => {
|
|
16
|
+
const err = new Error('boom');
|
|
17
|
+
expect(serializeError(err)).toEqual({ name: 'Error', message: 'boom', stack: err.stack, code: undefined, cause: undefined });
|
|
18
|
+
});
|
|
19
|
+
it('uses the subclass constructor name', () => {
|
|
20
|
+
const err = new TypeError('bad type');
|
|
21
|
+
expect(serializeError(err).name).toBe('TypeError');
|
|
22
|
+
expect(serializeError(err).message).toBe('bad type');
|
|
23
|
+
});
|
|
24
|
+
it('includes string code when set on the error', () => {
|
|
25
|
+
const err = new Error('e');
|
|
26
|
+
err.code = 'ENOENT';
|
|
27
|
+
expect(serializeError(err).code).toBe('ENOENT');
|
|
28
|
+
});
|
|
29
|
+
it('serializes Error cause as a nested plain object', () => {
|
|
30
|
+
const root = new Error('root');
|
|
31
|
+
const leaf = new TypeError('leaf');
|
|
32
|
+
root.cause = leaf;
|
|
33
|
+
expect(serializeError(root)).toEqual({
|
|
34
|
+
name: 'Error',
|
|
35
|
+
message: 'root',
|
|
36
|
+
stack: root.stack,
|
|
37
|
+
code: undefined,
|
|
38
|
+
cause: {
|
|
39
|
+
name: 'TypeError',
|
|
40
|
+
message: 'leaf',
|
|
41
|
+
stack: leaf.stack,
|
|
42
|
+
code: undefined,
|
|
43
|
+
cause: undefined
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
it('does not recurse into cause when initial depth is already past the limit', () => {
|
|
48
|
+
const inner = new Error('inner');
|
|
49
|
+
const outer = new Error('outer');
|
|
50
|
+
outer.cause = inner;
|
|
51
|
+
expect(serializeError(outer, 6).cause).toBe('<Max recursion depth reached>');
|
|
52
|
+
});
|
|
53
|
+
it('serializes up to five nested Error causes without hitting the sentinel', () => {
|
|
54
|
+
const root = createMultiLevelError(5);
|
|
55
|
+
const innermost = walkSerializedCause(serializeError(root), 4);
|
|
56
|
+
expect(innermost.message).toBe('level-5');
|
|
57
|
+
expect(innermost.cause).toBeUndefined();
|
|
58
|
+
});
|
|
59
|
+
it('replaces cause with the max-depth sentinel on the sixth nested Error', () => {
|
|
60
|
+
const root = createMultiLevelError(6);
|
|
61
|
+
const innermost = walkSerializedCause(serializeError(root), 5);
|
|
62
|
+
expect(innermost.message).toBe('level-6');
|
|
63
|
+
expect(innermost.cause).toBe('<Max recursion depth reached>');
|
|
64
|
+
});
|
|
65
|
+
it('does not expose a seventh error when the chain is longer than the limit', () => {
|
|
66
|
+
const root = createMultiLevelError(7);
|
|
67
|
+
const innermost = walkSerializedCause(serializeError(root), 5);
|
|
68
|
+
expect(innermost.message).toBe('level-6');
|
|
69
|
+
expect(innermost.cause).toBe('<Max recursion depth reached>');
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('redactHeaders', () => {
|
|
73
|
+
it('redacts sensitive headers case-insensitively', () => {
|
|
74
|
+
const headers = new Headers([
|
|
75
|
+
['Authorization', 'Bearer token123'],
|
|
76
|
+
['X-API-Key', 'secret-key'],
|
|
77
|
+
['apikey', 'another-secret'],
|
|
78
|
+
['X-Auth-Token', 'auth-token'],
|
|
79
|
+
['Secret-Header', 'top-secret'],
|
|
80
|
+
['Password', 'password123'],
|
|
81
|
+
['Private-Key', 'private-key-data'],
|
|
82
|
+
['Cookie', 'session=abc123'],
|
|
83
|
+
['Content-Type', 'application/json'],
|
|
84
|
+
['User-Agent', 'test-agent']
|
|
85
|
+
]);
|
|
86
|
+
expect(redactHeaders(headers)).toEqual({
|
|
87
|
+
authorization: '[REDACTED]',
|
|
88
|
+
'x-api-key': '[REDACTED]',
|
|
89
|
+
apikey: '[REDACTED]',
|
|
90
|
+
'x-auth-token': '[REDACTED]',
|
|
91
|
+
'secret-header': '[REDACTED]',
|
|
92
|
+
password: '[REDACTED]',
|
|
93
|
+
'private-key': '[REDACTED]',
|
|
94
|
+
cookie: '[REDACTED]',
|
|
95
|
+
'content-type': 'application/json',
|
|
96
|
+
'user-agent': 'test-agent'
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
it('leaves non-sensitive headers unchanged', () => {
|
|
100
|
+
const headers = new Headers({
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
Accept: 'application/json',
|
|
103
|
+
'Cache-Control': 'no-cache'
|
|
104
|
+
});
|
|
105
|
+
expect(redactHeaders(headers)).toEqual({
|
|
106
|
+
'content-type': 'application/json',
|
|
107
|
+
accept: 'application/json',
|
|
108
|
+
'cache-control': 'no-cache'
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
it('handles empty Headers', () => {
|
|
112
|
+
expect(redactHeaders(new Headers())).toEqual({});
|
|
113
|
+
});
|
|
114
|
+
it('redacts sensitive keys even when values are empty', () => {
|
|
115
|
+
const headers = new Headers([
|
|
116
|
+
['Authorization', ''],
|
|
117
|
+
['Content-Type', 'application/json'],
|
|
118
|
+
['X-API-Key', '']
|
|
119
|
+
]);
|
|
120
|
+
expect(redactHeaders(headers)).toEqual({
|
|
121
|
+
authorization: '[REDACTED]',
|
|
122
|
+
'content-type': 'application/json',
|
|
123
|
+
'x-api-key': '[REDACTED]'
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
it('does not redact benign names that only contained sensitive substrings before', () => {
|
|
127
|
+
const headers = new Headers({
|
|
128
|
+
Keyboard: 'qwerty',
|
|
129
|
+
Secretary: 'admin',
|
|
130
|
+
Tokens: 'abc123',
|
|
131
|
+
'Content-Length': '123'
|
|
132
|
+
});
|
|
133
|
+
expect(redactHeaders(headers)).toEqual({
|
|
134
|
+
keyboard: 'qwerty',
|
|
135
|
+
secretary: 'admin',
|
|
136
|
+
tokens: 'abc123',
|
|
137
|
+
'content-length': '123'
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
it('does not redact exempt headers but still redacts any header whose name contains a key segment', () => {
|
|
141
|
+
const headers = new Headers([
|
|
142
|
+
['X-Csrf-Token', 'abc'],
|
|
143
|
+
['Public-Key-Pins', 'pin-sha256=dummy'],
|
|
144
|
+
['ratelimit-tokens-left', '9'],
|
|
145
|
+
['Cache-Key', 'lookup-1'],
|
|
146
|
+
['X-Auth-Token', 'real-secret']
|
|
147
|
+
]);
|
|
148
|
+
expect(redactHeaders(headers)).toEqual({
|
|
149
|
+
'x-csrf-token': 'abc',
|
|
150
|
+
'public-key-pins': 'pin-sha256=dummy',
|
|
151
|
+
'ratelimit-tokens-left': '9',
|
|
152
|
+
'cache-key': '[REDACTED]',
|
|
153
|
+
'x-auth-token': '[REDACTED]'
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('key suffix pattern /key(?![a-z0-9])/i', () => {
|
|
157
|
+
it.each([
|
|
158
|
+
['x-api-key', 'hyphen before key at end'],
|
|
159
|
+
['apikey', 'single segment ending in key'],
|
|
160
|
+
['X-LicenseKey', 'camel segment ending in Key'],
|
|
161
|
+
['webhook-signing-key', 'key before end'],
|
|
162
|
+
['pre-key-post', 'key surrounded by hyphens'],
|
|
163
|
+
['encryption_key', 'underscore after key'],
|
|
164
|
+
['signing-key-id', 'key in middle of kebab name'],
|
|
165
|
+
['monkey', 'key followed by end of string (substring key)'],
|
|
166
|
+
['turkey-vulture', 'key in first segment before hyphen'],
|
|
167
|
+
['v1__key', 'non-alnum before key']
|
|
168
|
+
])('redacts %s (%s)', headerName => {
|
|
169
|
+
const headers = new Headers([[headerName, 'secret-value']]);
|
|
170
|
+
const out = redactHeaders(headers);
|
|
171
|
+
const canonical = Object.keys(out)[0];
|
|
172
|
+
expect(out[canonical]).toBe('[REDACTED]');
|
|
173
|
+
});
|
|
174
|
+
it.each([
|
|
175
|
+
['keyboard', 'key followed by letter b'],
|
|
176
|
+
['KeyAccountId', 'key followed by letter a'],
|
|
177
|
+
['WhiskeyBar', 'key followed by letter b'],
|
|
178
|
+
['my-keyring', 'key followed by letter r'],
|
|
179
|
+
['Cache-Control', 'no key substring with allowed lookahead']
|
|
180
|
+
])('does not redact %s (%s)', headerName => {
|
|
181
|
+
const headers = new Headers([[headerName, 'visible']]);
|
|
182
|
+
const out = redactHeaders(headers);
|
|
183
|
+
const canonical = Object.keys(out)[0];
|
|
184
|
+
expect(out[canonical]).toBe('visible');
|
|
185
|
+
});
|
|
186
|
+
it('still applies exempt list when the key rule would match (e.g. public-key-pins)', () => {
|
|
187
|
+
const headers = new Headers([['Public-Key-Pins', 'pins-value']]);
|
|
188
|
+
expect(redactHeaders(headers)).toEqual({ 'public-key-pins': 'pins-value' });
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
describe('parseBody', () => {
|
|
193
|
+
it('parses JSON when content-type is application/json', async () => {
|
|
194
|
+
const response = new Response(JSON.stringify({ ok: true }), {
|
|
195
|
+
headers: { 'content-type': 'application/json' }
|
|
196
|
+
});
|
|
197
|
+
await expect(parseBody(response)).resolves.toEqual({ ok: true });
|
|
198
|
+
});
|
|
199
|
+
it('parses JSON when content-type includes charset', async () => {
|
|
200
|
+
const response = new Response(JSON.stringify([1, 2]), {
|
|
201
|
+
headers: { 'content-type': 'application/json; charset=utf-8' }
|
|
202
|
+
});
|
|
203
|
+
await expect(parseBody(response)).resolves.toEqual([1, 2]);
|
|
204
|
+
});
|
|
205
|
+
it('returns text when content-type is not JSON', async () => {
|
|
206
|
+
const response = new Response('hello', {
|
|
207
|
+
headers: { 'content-type': 'text/plain' }
|
|
208
|
+
});
|
|
209
|
+
await expect(parseBody(response)).resolves.toBe('hello');
|
|
210
|
+
});
|
|
211
|
+
it('uses text branch when content-type is missing', async () => {
|
|
212
|
+
const response = new Response('plain');
|
|
213
|
+
await expect(parseBody(response)).resolves.toBe('plain');
|
|
214
|
+
});
|
|
215
|
+
it('returns empty string for empty body (text)', async () => {
|
|
216
|
+
const response = new Response('', { headers: { 'content-type': 'text/plain' } });
|
|
217
|
+
await expect(parseBody(response)).resolves.toBe('');
|
|
218
|
+
});
|
|
219
|
+
it('returns empty string for empty body even when content-type is application/json', async () => {
|
|
220
|
+
const response = new Response('', { headers: { 'content-type': 'application/json' } });
|
|
221
|
+
await expect(parseBody(response)).resolves.toBe('');
|
|
222
|
+
});
|
|
223
|
+
it('returns raw text when content-type is application/json but the body is not valid JSON', async () => {
|
|
224
|
+
const raw = '{ not json';
|
|
225
|
+
const response = new Response(raw, { headers: { 'content-type': 'application/json' } });
|
|
226
|
+
const result = await parseBody(response);
|
|
227
|
+
expect(result).toBe(raw);
|
|
228
|
+
expect(result).toBeTypeOf('string');
|
|
229
|
+
});
|
|
230
|
+
it('does not consume the original body (clone)', async () => {
|
|
231
|
+
const response = new Response('read-me', { headers: { 'content-type': 'text/plain' } });
|
|
232
|
+
await parseBody(response);
|
|
233
|
+
await expect(response.text()).resolves.toBe('read-me');
|
|
234
|
+
});
|
|
235
|
+
it('parses JSON Request body', async () => {
|
|
236
|
+
const request = new Request('https://ex.com', {
|
|
237
|
+
method: 'POST',
|
|
238
|
+
headers: { 'content-type': 'application/json' },
|
|
239
|
+
body: JSON.stringify({ a: 1 })
|
|
240
|
+
});
|
|
241
|
+
await expect(parseBody(request)).resolves.toEqual({ a: 1 });
|
|
242
|
+
});
|
|
243
|
+
it('returns raw text for invalid JSON on a Request with application/json', async () => {
|
|
244
|
+
const raw = '{';
|
|
245
|
+
const request = new Request('https://ex.com', {
|
|
246
|
+
method: 'POST',
|
|
247
|
+
headers: { 'content-type': 'application/json' },
|
|
248
|
+
body: raw
|
|
249
|
+
});
|
|
250
|
+
await expect(parseBody(request)).resolves.toBe(raw);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
});
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Options } from 'ky';
|
|
|
2
2
|
/**
|
|
3
3
|
* Creates a ky client.
|
|
4
4
|
*
|
|
5
|
-
* This client
|
|
5
|
+
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
@@ -24,3 +24,4 @@ import type { Options } from 'ky';
|
|
|
24
24
|
export declare function httpClient(options?: Options): import("ky").KyInstance;
|
|
25
25
|
export { HTTPError, TimeoutError } from 'ky';
|
|
26
26
|
export type { Options as HttpClientOptions } from 'ky';
|
|
27
|
+
export * from './fetch/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,32 +1,9 @@
|
|
|
1
1
|
import ky from 'ky';
|
|
2
|
-
import {
|
|
3
|
-
import { applyFetchErrorTracing } from '#hooks/trace_error.js';
|
|
4
|
-
const baseHttpClient = ky.create({
|
|
5
|
-
hooks: {
|
|
6
|
-
beforeRequest: [
|
|
7
|
-
assignRequestId,
|
|
8
|
-
traceRequest
|
|
9
|
-
],
|
|
10
|
-
afterResponse: [
|
|
11
|
-
traceResponse
|
|
12
|
-
],
|
|
13
|
-
beforeError: [
|
|
14
|
-
traceError
|
|
15
|
-
]
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
const applyDefaultOptions = (userOptions) => (parentOptions) => {
|
|
19
|
-
const kyFetch = parentOptions.fetch || globalThis.fetch.bind(globalThis);
|
|
20
|
-
const patchedFetch = applyFetchErrorTracing(kyFetch);
|
|
21
|
-
return {
|
|
22
|
-
fetch: patchedFetch,
|
|
23
|
-
...userOptions
|
|
24
|
-
};
|
|
25
|
-
};
|
|
2
|
+
import { fetch as customFetch } from './fetch/index.js';
|
|
26
3
|
/**
|
|
27
4
|
* Creates a ky client.
|
|
28
5
|
*
|
|
29
|
-
* This client
|
|
6
|
+
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
30
7
|
*
|
|
31
8
|
* @example
|
|
32
9
|
* ```ts
|
|
@@ -46,6 +23,7 @@ const applyDefaultOptions = (userOptions) => (parentOptions) => {
|
|
|
46
23
|
* @returns A ky instance extended with Output.ai tracing hooks.
|
|
47
24
|
*/
|
|
48
25
|
export function httpClient(options = {}) {
|
|
49
|
-
return
|
|
26
|
+
return ky.create({ fetch: customFetch, ...options });
|
|
50
27
|
}
|
|
51
28
|
export { HTTPError, TimeoutError } from 'ky';
|
|
29
|
+
export * from './fetch/index.js';
|