@outputai/http 0.10.1-dev.b7b2fbe.0 → 0.10.1-next.2caa4a1.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/events.d.ts +20 -0
- package/dist/fetch/events.js +4 -0
- package/dist/fetch/events.spec.js +64 -0
- package/dist/fetch/index.d.ts +10 -11
- package/dist/fetch/index.default_dispatcher.spec.d.ts +1 -0
- package/dist/fetch/index.default_dispatcher.spec.js +47 -0
- package/dist/fetch/index.js +32 -19
- package/dist/fetch/index.spec.js +114 -11
- package/dist/fetch/logger.d.ts +16 -66
- package/dist/fetch/logger.js +16 -69
- package/dist/fetch/logger.spec.js +80 -179
- package/dist/fetch/utils.d.ts +4 -45
- package/dist/fetch/utils.js +14 -11
- package/dist/fetch/utils.spec.js +68 -231
- package/dist/index.d.ts +5 -27
- package/dist/index.js +5 -29
- package/dist/ky/index.d.ts +24 -0
- package/dist/ky/index.js +25 -0
- package/dist/ky/index.spec.d.ts +1 -0
- package/dist/ky/index.spec.js +34 -0
- package/package.json +10 -7
- package/dist/index.spec.js +0 -13
- /package/dist/{index.spec.d.ts → fetch/events.spec.d.ts} +0 -0
package/dist/fetch/utils.spec.js
CHANGED
|
@@ -1,282 +1,119 @@
|
|
|
1
|
-
import { describe,
|
|
2
|
-
import {
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Headers, Request, Response } from 'undici';
|
|
3
3
|
import { requestIdSymbol } from '../consts.js';
|
|
4
4
|
import { addRequestIdToResponse, parseBody, redactHeaders, serializeError } from './utils.js';
|
|
5
|
-
const
|
|
6
|
-
new Error(
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
return
|
|
11
|
-
}
|
|
12
|
-
return
|
|
5
|
+
const createErrorChain = levels => {
|
|
6
|
+
const root = new Error('level-1');
|
|
7
|
+
Array.from({ length: levels - 1 }, (_, index) => index + 2).reduce((current, level) => {
|
|
8
|
+
const cause = new Error(`level-${level}`);
|
|
9
|
+
current.cause = cause;
|
|
10
|
+
return cause;
|
|
11
|
+
}, root);
|
|
12
|
+
return root;
|
|
13
13
|
};
|
|
14
|
-
describe('
|
|
14
|
+
describe('instrumented_fetch/utils', () => {
|
|
15
15
|
describe('serializeError', () => {
|
|
16
|
-
it('serializes
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const err = new TypeError('bad type');
|
|
22
|
-
expect(serializeError(err).name).toBe('TypeError');
|
|
23
|
-
expect(serializeError(err).message).toBe('bad type');
|
|
24
|
-
});
|
|
25
|
-
it('includes string code when set on the error', () => {
|
|
26
|
-
const err = new Error('e');
|
|
27
|
-
err.code = 'ENOENT';
|
|
28
|
-
expect(serializeError(err).code).toBe('ENOENT');
|
|
29
|
-
});
|
|
30
|
-
it('serializes Error cause as a nested plain object', () => {
|
|
31
|
-
const root = new Error('root');
|
|
32
|
-
const leaf = new TypeError('leaf');
|
|
33
|
-
root.cause = leaf;
|
|
34
|
-
expect(serializeError(root)).toEqual({
|
|
16
|
+
it('serializes standard error properties and a nested cause', () => {
|
|
17
|
+
const cause = new TypeError('cause');
|
|
18
|
+
const error = new Error('failure', { cause });
|
|
19
|
+
error.code = 'E_FAILURE';
|
|
20
|
+
expect(serializeError(error)).toEqual({
|
|
35
21
|
name: 'Error',
|
|
36
|
-
message: '
|
|
37
|
-
stack:
|
|
38
|
-
code:
|
|
22
|
+
message: 'failure',
|
|
23
|
+
stack: error.stack,
|
|
24
|
+
code: 'E_FAILURE',
|
|
39
25
|
cause: {
|
|
40
26
|
name: 'TypeError',
|
|
41
|
-
message: '
|
|
42
|
-
stack:
|
|
27
|
+
message: 'cause',
|
|
28
|
+
stack: cause.stack,
|
|
43
29
|
code: undefined,
|
|
44
30
|
cause: undefined
|
|
45
31
|
}
|
|
46
32
|
});
|
|
47
33
|
});
|
|
48
|
-
it('
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
expect(
|
|
53
|
-
});
|
|
54
|
-
it('serializes up to five nested Error causes without hitting the sentinel', () => {
|
|
55
|
-
const root = createMultiLevelError(5);
|
|
56
|
-
const innermost = walkSerializedCause(serializeError(root), 4);
|
|
57
|
-
expect(innermost.message).toBe('level-5');
|
|
58
|
-
expect(innermost.cause).toBeUndefined();
|
|
59
|
-
});
|
|
60
|
-
it('replaces cause with the max-depth sentinel on the sixth nested Error', () => {
|
|
61
|
-
const root = createMultiLevelError(6);
|
|
62
|
-
const innermost = walkSerializedCause(serializeError(root), 5);
|
|
63
|
-
expect(innermost.message).toBe('level-6');
|
|
64
|
-
expect(innermost.cause).toBe('<Max recursion depth reached>');
|
|
65
|
-
});
|
|
66
|
-
it('does not expose a seventh error when the chain is longer than the limit', () => {
|
|
67
|
-
const root = createMultiLevelError(7);
|
|
68
|
-
const innermost = walkSerializedCause(serializeError(root), 5);
|
|
69
|
-
expect(innermost.message).toBe('level-6');
|
|
70
|
-
expect(innermost.cause).toBe('<Max recursion depth reached>');
|
|
34
|
+
it('limits the serialized cause depth', () => {
|
|
35
|
+
const serialized = serializeError(createErrorChain(7));
|
|
36
|
+
const current = Array.from({ length: 5 }).reduce(cause => cause.cause, serialized);
|
|
37
|
+
expect(current.message).toBe('level-6');
|
|
38
|
+
expect(current.cause).toBe('<Max recursion depth reached>');
|
|
71
39
|
});
|
|
72
40
|
});
|
|
73
41
|
describe('redactHeaders', () => {
|
|
74
|
-
it('redacts sensitive
|
|
75
|
-
const headers = new Headers([
|
|
76
|
-
['Authorization', 'Bearer token123'],
|
|
77
|
-
['X-API-Key', 'secret-key'],
|
|
78
|
-
['apikey', 'another-secret'],
|
|
79
|
-
['X-Auth-Token', 'auth-token'],
|
|
80
|
-
['Secret-Header', 'top-secret'],
|
|
81
|
-
['Password', 'password123'],
|
|
82
|
-
['Private-Key', 'private-key-data'],
|
|
83
|
-
['Cookie', 'session=abc123'],
|
|
84
|
-
['Content-Type', 'application/json'],
|
|
85
|
-
['User-Agent', 'test-agent']
|
|
86
|
-
]);
|
|
87
|
-
expect(redactHeaders(headers)).toEqual({
|
|
88
|
-
authorization: '[REDACTED]',
|
|
89
|
-
'x-api-key': '[REDACTED]',
|
|
90
|
-
apikey: '[REDACTED]',
|
|
91
|
-
'x-auth-token': '[REDACTED]',
|
|
92
|
-
'secret-header': '[REDACTED]',
|
|
93
|
-
password: '[REDACTED]',
|
|
94
|
-
'private-key': '[REDACTED]',
|
|
95
|
-
cookie: '[REDACTED]',
|
|
96
|
-
'content-type': 'application/json',
|
|
97
|
-
'user-agent': 'test-agent'
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
it('leaves non-sensitive headers unchanged', () => {
|
|
42
|
+
it('redacts sensitive header names while preserving safe values', () => {
|
|
101
43
|
const headers = new Headers({
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
expect(redactHeaders(headers)).toEqual({
|
|
107
|
-
'content-type': 'application/json',
|
|
108
|
-
accept: 'application/json',
|
|
109
|
-
'cache-control': 'no-cache'
|
|
44
|
+
authorization: 'Bearer secret',
|
|
45
|
+
'x-api-key': 'secret',
|
|
46
|
+
cookie: 'session=secret',
|
|
47
|
+
'content-type': 'application/json'
|
|
110
48
|
});
|
|
111
|
-
});
|
|
112
|
-
it('handles empty Headers', () => {
|
|
113
|
-
expect(redactHeaders(new Headers())).toEqual({});
|
|
114
|
-
});
|
|
115
|
-
it('redacts sensitive keys even when values are empty', () => {
|
|
116
|
-
const headers = new Headers([
|
|
117
|
-
['Authorization', ''],
|
|
118
|
-
['Content-Type', 'application/json'],
|
|
119
|
-
['X-API-Key', '']
|
|
120
|
-
]);
|
|
121
49
|
expect(redactHeaders(headers)).toEqual({
|
|
122
50
|
authorization: '[REDACTED]',
|
|
123
|
-
'
|
|
124
|
-
|
|
51
|
+
'x-api-key': '[REDACTED]',
|
|
52
|
+
cookie: '[REDACTED]',
|
|
53
|
+
'content-type': 'application/json'
|
|
125
54
|
});
|
|
126
55
|
});
|
|
127
|
-
it('does not redact
|
|
56
|
+
it('does not redact exempt or substring-only header names', () => {
|
|
128
57
|
const headers = new Headers({
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
58
|
+
'x-csrf-token': 'csrf-value',
|
|
59
|
+
'public-key-pins': 'pin-value',
|
|
60
|
+
keyboard: 'keyboard-value',
|
|
61
|
+
tokens: 'token-count'
|
|
133
62
|
});
|
|
134
63
|
expect(redactHeaders(headers)).toEqual({
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
it('does not redact exempt headers but still redacts any header whose name contains a key segment', () => {
|
|
142
|
-
const headers = new Headers([
|
|
143
|
-
['X-Csrf-Token', 'abc'],
|
|
144
|
-
['Public-Key-Pins', 'pin-sha256=dummy'],
|
|
145
|
-
['ratelimit-tokens-left', '9'],
|
|
146
|
-
['Cache-Key', 'lookup-1'],
|
|
147
|
-
['X-Auth-Token', 'real-secret']
|
|
148
|
-
]);
|
|
149
|
-
expect(redactHeaders(headers)).toEqual({
|
|
150
|
-
'x-csrf-token': 'abc',
|
|
151
|
-
'public-key-pins': 'pin-sha256=dummy',
|
|
152
|
-
'ratelimit-tokens-left': '9',
|
|
153
|
-
'cache-key': '[REDACTED]',
|
|
154
|
-
'x-auth-token': '[REDACTED]'
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
describe('key suffix pattern /key(?![a-z0-9])/i', () => {
|
|
158
|
-
it.each([
|
|
159
|
-
['x-api-key', 'hyphen before key at end'],
|
|
160
|
-
['apikey', 'single segment ending in key'],
|
|
161
|
-
['X-LicenseKey', 'camel segment ending in Key'],
|
|
162
|
-
['webhook-signing-key', 'key before end'],
|
|
163
|
-
['pre-key-post', 'key surrounded by hyphens'],
|
|
164
|
-
['encryption_key', 'underscore after key'],
|
|
165
|
-
['signing-key-id', 'key in middle of kebab name'],
|
|
166
|
-
['monkey', 'key followed by end of string (substring key)'],
|
|
167
|
-
['turkey-vulture', 'key in first segment before hyphen'],
|
|
168
|
-
['v1__key', 'non-alnum before key']
|
|
169
|
-
])('redacts %s (%s)', headerName => {
|
|
170
|
-
const headers = new Headers([[headerName, 'secret-value']]);
|
|
171
|
-
const out = redactHeaders(headers);
|
|
172
|
-
const canonical = Object.keys(out)[0];
|
|
173
|
-
expect(out[canonical]).toBe('[REDACTED]');
|
|
174
|
-
});
|
|
175
|
-
it.each([
|
|
176
|
-
['keyboard', 'key followed by letter b'],
|
|
177
|
-
['KeyAccountId', 'key followed by letter a'],
|
|
178
|
-
['WhiskeyBar', 'key followed by letter b'],
|
|
179
|
-
['my-keyring', 'key followed by letter r'],
|
|
180
|
-
['Cache-Control', 'no key substring with allowed lookahead']
|
|
181
|
-
])('does not redact %s (%s)', headerName => {
|
|
182
|
-
const headers = new Headers([[headerName, 'visible']]);
|
|
183
|
-
const out = redactHeaders(headers);
|
|
184
|
-
const canonical = Object.keys(out)[0];
|
|
185
|
-
expect(out[canonical]).toBe('visible');
|
|
186
|
-
});
|
|
187
|
-
it('still applies exempt list when the key rule would match (e.g. public-key-pins)', () => {
|
|
188
|
-
const headers = new Headers([['Public-Key-Pins', 'pins-value']]);
|
|
189
|
-
expect(redactHeaders(headers)).toEqual({ 'public-key-pins': 'pins-value' });
|
|
64
|
+
'x-csrf-token': 'csrf-value',
|
|
65
|
+
'public-key-pins': 'pin-value',
|
|
66
|
+
keyboard: 'keyboard-value',
|
|
67
|
+
tokens: 'token-count'
|
|
190
68
|
});
|
|
191
69
|
});
|
|
192
70
|
});
|
|
193
71
|
describe('parseBody', () => {
|
|
194
|
-
it('parses JSON
|
|
72
|
+
it('parses JSON bodies without consuming the original', async () => {
|
|
195
73
|
const response = new Response(JSON.stringify({ ok: true }), {
|
|
196
|
-
headers: { 'content-type': 'application/json' }
|
|
197
|
-
});
|
|
198
|
-
await expect(parseBody(response)).resolves.toEqual({ ok: true });
|
|
199
|
-
});
|
|
200
|
-
it('parses JSON when content-type includes charset', async () => {
|
|
201
|
-
const response = new Response(JSON.stringify([1, 2]), {
|
|
202
74
|
headers: { 'content-type': 'application/json; charset=utf-8' }
|
|
203
75
|
});
|
|
204
|
-
await expect(parseBody(response)).resolves.toEqual(
|
|
76
|
+
await expect(parseBody(response)).resolves.toEqual({ ok: true });
|
|
77
|
+
await expect(response.json()).resolves.toEqual({ ok: true });
|
|
205
78
|
});
|
|
206
|
-
it('returns
|
|
207
|
-
const
|
|
79
|
+
it('returns non-JSON and invalid JSON bodies as text', async () => {
|
|
80
|
+
const textResponse = new Response('plain text', {
|
|
208
81
|
headers: { 'content-type': 'text/plain' }
|
|
209
82
|
});
|
|
210
|
-
|
|
211
|
-
});
|
|
212
|
-
it('uses text branch when content-type is missing', async () => {
|
|
213
|
-
const response = new Response('plain');
|
|
214
|
-
await expect(parseBody(response)).resolves.toBe('plain');
|
|
215
|
-
});
|
|
216
|
-
it('returns empty string for empty body (text)', async () => {
|
|
217
|
-
const response = new Response('', { headers: { 'content-type': 'text/plain' } });
|
|
218
|
-
await expect(parseBody(response)).resolves.toBe('');
|
|
219
|
-
});
|
|
220
|
-
it('returns empty string for empty body even when content-type is application/json', async () => {
|
|
221
|
-
const response = new Response('', { headers: { 'content-type': 'application/json' } });
|
|
222
|
-
await expect(parseBody(response)).resolves.toBe('');
|
|
223
|
-
});
|
|
224
|
-
it('returns raw text when content-type is application/json but the body is not valid JSON', async () => {
|
|
225
|
-
const raw = '{ not json';
|
|
226
|
-
const response = new Response(raw, { headers: { 'content-type': 'application/json' } });
|
|
227
|
-
const result = await parseBody(response);
|
|
228
|
-
expect(result).toBe(raw);
|
|
229
|
-
expect(result).toBeTypeOf('string');
|
|
230
|
-
});
|
|
231
|
-
it('does not consume the original body (clone)', async () => {
|
|
232
|
-
const response = new Response('read-me', { headers: { 'content-type': 'text/plain' } });
|
|
233
|
-
await parseBody(response);
|
|
234
|
-
await expect(response.text()).resolves.toBe('read-me');
|
|
235
|
-
});
|
|
236
|
-
it('parses JSON Request body', async () => {
|
|
237
|
-
const request = new Request('https://ex.com', {
|
|
83
|
+
const invalidJsonRequest = new Request('https://example.com', {
|
|
238
84
|
method: 'POST',
|
|
239
85
|
headers: { 'content-type': 'application/json' },
|
|
240
|
-
body:
|
|
86
|
+
body: '{invalid'
|
|
241
87
|
});
|
|
242
|
-
await expect(parseBody(
|
|
88
|
+
await expect(parseBody(textResponse)).resolves.toBe('plain text');
|
|
89
|
+
await expect(parseBody(invalidJsonRequest)).resolves.toBe('{invalid');
|
|
243
90
|
});
|
|
244
|
-
it('returns
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
method: 'POST',
|
|
248
|
-
headers: { 'content-type': 'application/json' },
|
|
249
|
-
body: raw
|
|
91
|
+
it('returns an empty string for an empty JSON body', async () => {
|
|
92
|
+
const response = new Response('', {
|
|
93
|
+
headers: { 'content-type': 'application/json' }
|
|
250
94
|
});
|
|
251
|
-
await expect(parseBody(
|
|
95
|
+
await expect(parseBody(response)).resolves.toBe('');
|
|
252
96
|
});
|
|
253
97
|
});
|
|
254
98
|
describe('addRequestIdToResponse', () => {
|
|
255
|
-
it('stores
|
|
256
|
-
const response = new Response('ok');
|
|
257
|
-
addRequestIdToResponse(response, 'req-123');
|
|
258
|
-
expect(response[requestIdSymbol]).toBe('req-123');
|
|
259
|
-
});
|
|
260
|
-
it('defines request id as non-enumerable, non-writable and non-configurable', () => {
|
|
99
|
+
it('stores an immutable, non-enumerable request id', () => {
|
|
261
100
|
const response = new Response('ok');
|
|
262
|
-
addRequestIdToResponse(response, '
|
|
263
|
-
|
|
264
|
-
expect(
|
|
265
|
-
|
|
101
|
+
addRequestIdToResponse(response, 'request-1');
|
|
102
|
+
expect(response[requestIdSymbol]).toBe('request-1');
|
|
103
|
+
expect(Object.getOwnPropertyDescriptor(response, requestIdSymbol)).toEqual({
|
|
104
|
+
value: 'request-1',
|
|
266
105
|
enumerable: false,
|
|
267
106
|
configurable: false,
|
|
268
|
-
writable: false
|
|
269
|
-
value: 'req-456'
|
|
107
|
+
writable: false
|
|
270
108
|
});
|
|
271
109
|
});
|
|
272
|
-
it('propagates the request id
|
|
110
|
+
it('propagates the request id through repeated clones', () => {
|
|
273
111
|
const response = new Response('ok');
|
|
274
|
-
addRequestIdToResponse(response, '
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
expect(grandchild[requestIdSymbol]).toBe('req-789');
|
|
112
|
+
addRequestIdToResponse(response, 'request-clone');
|
|
113
|
+
const clone = response.clone();
|
|
114
|
+
const grandchild = clone.clone();
|
|
115
|
+
expect(clone[requestIdSymbol]).toBe('request-clone');
|
|
116
|
+
expect(grandchild[requestIdSymbol]).toBe('request-clone');
|
|
280
117
|
});
|
|
281
118
|
});
|
|
282
119
|
});
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Options } from 'ky';
|
|
2
1
|
export type HttpRequestEvent = {
|
|
3
2
|
requestId: string;
|
|
4
3
|
method: string;
|
|
@@ -13,30 +12,9 @@ export type HttpRequestCostEvent = {
|
|
|
13
12
|
url: string;
|
|
14
13
|
total: number;
|
|
15
14
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
*
|
|
19
|
-
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
20
|
-
*
|
|
21
|
-
* @example
|
|
22
|
-
* ```ts
|
|
23
|
-
* import { httpClient } from '@outputai/http';
|
|
24
|
-
*
|
|
25
|
-
* const client = httpClient({
|
|
26
|
-
* prefixUrl: 'https://api.example.com',
|
|
27
|
-
* timeout: 30000,
|
|
28
|
-
* retry: { limit: 3 }
|
|
29
|
-
* });
|
|
30
|
-
*
|
|
31
|
-
* const response = await client.get('users/1');
|
|
32
|
-
* const data = await response.json();
|
|
33
|
-
* ```
|
|
34
|
-
*
|
|
35
|
-
* @param options - The ky options to extend the base client.
|
|
36
|
-
* @returns A ky instance extended with Output.ai tracing hooks.
|
|
37
|
-
*/
|
|
38
|
-
export declare function httpClient(options?: Options): import("ky").KyInstance;
|
|
39
|
-
export { HTTPError, TimeoutError } from 'ky';
|
|
40
|
-
export type { Options as HttpClientOptions } from 'ky';
|
|
41
|
-
export * from './fetch/index.js';
|
|
15
|
+
export { outputFetch } from './fetch/index.js';
|
|
16
|
+
export { createKyClient } from './ky/index.js';
|
|
42
17
|
export { addRequestCost } from './cost.js';
|
|
18
|
+
/** Re-export ky library for convenience. */
|
|
19
|
+
export * as ky from 'ky';
|
|
20
|
+
export * as undici from 'undici';
|
package/dist/index.js
CHANGED
|
@@ -1,30 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Creates a ky client.
|
|
5
|
-
*
|
|
6
|
-
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
7
|
-
*
|
|
8
|
-
* @example
|
|
9
|
-
* ```ts
|
|
10
|
-
* import { httpClient } from '@outputai/http';
|
|
11
|
-
*
|
|
12
|
-
* const client = httpClient({
|
|
13
|
-
* prefixUrl: 'https://api.example.com',
|
|
14
|
-
* timeout: 30000,
|
|
15
|
-
* retry: { limit: 3 }
|
|
16
|
-
* });
|
|
17
|
-
*
|
|
18
|
-
* const response = await client.get('users/1');
|
|
19
|
-
* const data = await response.json();
|
|
20
|
-
* ```
|
|
21
|
-
*
|
|
22
|
-
* @param options - The ky options to extend the base client.
|
|
23
|
-
* @returns A ky instance extended with Output.ai tracing hooks.
|
|
24
|
-
*/
|
|
25
|
-
export function httpClient(options = {}) {
|
|
26
|
-
return ky.create({ fetch: customFetch, ...options });
|
|
27
|
-
}
|
|
28
|
-
export { HTTPError, TimeoutError } from 'ky';
|
|
29
|
-
export * from './fetch/index.js';
|
|
1
|
+
export { outputFetch } from './fetch/index.js';
|
|
2
|
+
export { createKyClient } from './ky/index.js';
|
|
30
3
|
export { addRequestCost } from './cost.js';
|
|
4
|
+
/** Re-export ky library for convenience. */
|
|
5
|
+
export * as ky from 'ky';
|
|
6
|
+
export * as undici from 'undici';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { KyInstance, Options } from 'ky';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a ky client.
|
|
4
|
+
*
|
|
5
|
+
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createKyClient } from '@outputai/http';
|
|
10
|
+
*
|
|
11
|
+
* const client = createKyClient({
|
|
12
|
+
* prefix: 'https://api.example.com',
|
|
13
|
+
* timeout: 30000,
|
|
14
|
+
* retry: { limit: 3 }
|
|
15
|
+
* });
|
|
16
|
+
*
|
|
17
|
+
* const response = await client.get('users/1');
|
|
18
|
+
* const data = await response.json();
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @param options - The ky options to extend the base client.
|
|
22
|
+
* @returns A ky instance extended with Output.ai tracing hooks.
|
|
23
|
+
*/
|
|
24
|
+
export declare const createKyClient: (options?: Options) => KyInstance;
|
package/dist/ky/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import ky from 'ky';
|
|
2
|
+
import { outputFetch } from '../fetch/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Creates a ky client.
|
|
5
|
+
*
|
|
6
|
+
* This client uses a custom fetch that introduces hooks to integrate with Output.ai tracing.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* import { createKyClient } from '@outputai/http';
|
|
11
|
+
*
|
|
12
|
+
* const client = createKyClient({
|
|
13
|
+
* prefix: 'https://api.example.com',
|
|
14
|
+
* timeout: 30000,
|
|
15
|
+
* retry: { limit: 3 }
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* const response = await client.get('users/1');
|
|
19
|
+
* const data = await response.json();
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @param options - The ky options to extend the base client.
|
|
23
|
+
* @returns A ky instance extended with Output.ai tracing hooks.
|
|
24
|
+
*/
|
|
25
|
+
export const createKyClient = (options = {}) => ky.create({ fetch: outputFetch, ...options });
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { outputFetch } from '../fetch/index.js';
|
|
3
|
+
const kyMock = vi.hoisted(() => ({
|
|
4
|
+
client: { get: vi.fn() },
|
|
5
|
+
create: vi.fn()
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('ky', () => ({
|
|
8
|
+
default: { create: kyMock.create }
|
|
9
|
+
}));
|
|
10
|
+
import { createKyClient } from './index.js';
|
|
11
|
+
describe('createKyClient', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
kyMock.create.mockReset();
|
|
14
|
+
kyMock.create.mockReturnValue(kyMock.client);
|
|
15
|
+
});
|
|
16
|
+
it('creates a Ky client using outputFetch', () => {
|
|
17
|
+
expect(createKyClient()).toBe(kyMock.client);
|
|
18
|
+
expect(kyMock.create).toHaveBeenCalledWith({ fetch: outputFetch });
|
|
19
|
+
});
|
|
20
|
+
it('forwards Ky options', () => {
|
|
21
|
+
const options = {
|
|
22
|
+
prefix: 'https://example.com',
|
|
23
|
+
timeout: 30_000,
|
|
24
|
+
retry: { limit: 3 }
|
|
25
|
+
};
|
|
26
|
+
createKyClient(options);
|
|
27
|
+
expect(kyMock.create).toHaveBeenCalledWith({ fetch: outputFetch, ...options });
|
|
28
|
+
});
|
|
29
|
+
it('allows callers to override fetch', () => {
|
|
30
|
+
const fetch = vi.fn(async () => new Response());
|
|
31
|
+
createKyClient({ fetch });
|
|
32
|
+
expect(kyMock.create).toHaveBeenCalledWith({ fetch });
|
|
33
|
+
});
|
|
34
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@outputai/http",
|
|
3
|
-
"version": "0.10.1-
|
|
3
|
+
"version": "0.10.1-next.2caa4a1.0",
|
|
4
4
|
"description": "Framework abstraction to make HTTP calls with tracing",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,18 +11,21 @@
|
|
|
11
11
|
"files": [
|
|
12
12
|
"dist"
|
|
13
13
|
],
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"ky": ">=2 <3",
|
|
16
|
+
"undici": ">=8 <9"
|
|
17
|
+
},
|
|
14
18
|
"dependencies": {
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
19
|
+
"@outputai/core": "0.10.1-next.2caa4a1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"ky": "2.0.2",
|
|
23
|
+
"undici": "8.9.0"
|
|
18
24
|
},
|
|
19
25
|
"license": "Apache-2.0",
|
|
20
26
|
"publishConfig": {
|
|
21
27
|
"access": "public"
|
|
22
28
|
},
|
|
23
|
-
"imports": {
|
|
24
|
-
"#*": "./dist/*"
|
|
25
|
-
},
|
|
26
29
|
"scripts": {
|
|
27
30
|
"build": "rm -rf ./dist && tsc"
|
|
28
31
|
}
|
package/dist/index.spec.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { httpClient } from './index.js';
|
|
3
|
-
describe('httpClient', () => {
|
|
4
|
-
it('passes the injected fetch to ky so requests use the custom implementation', async () => {
|
|
5
|
-
const spyFetch = vi.fn((_input, _init) => Promise.resolve(new Response(JSON.stringify({ ok: true }), { status: 200 })));
|
|
6
|
-
const client = httpClient({
|
|
7
|
-
fetch: spyFetch,
|
|
8
|
-
prefixUrl: 'https://example.com'
|
|
9
|
-
});
|
|
10
|
-
await client.get('path');
|
|
11
|
-
expect(spyFetch).toHaveBeenCalled();
|
|
12
|
-
});
|
|
13
|
-
});
|
|
File without changes
|