@outputai/http 0.10.1-next.69a0f11.0 → 0.10.1-next.af37678.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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function emitError({ requestId, method, url, status, durationMs }: {
|
|
2
|
+
requestId: any;
|
|
3
|
+
method: any;
|
|
4
|
+
url: any;
|
|
5
|
+
status: any;
|
|
6
|
+
durationMs: any;
|
|
7
|
+
}): boolean;
|
|
8
|
+
export function emitSuccess({ requestId, method, url, status, durationMs }: {
|
|
9
|
+
requestId: any;
|
|
10
|
+
method: any;
|
|
11
|
+
url: any;
|
|
12
|
+
status: any;
|
|
13
|
+
durationMs: any;
|
|
14
|
+
}): boolean;
|
|
15
|
+
export function emitFailure({ requestId, method, url, durationMs }: {
|
|
16
|
+
requestId: any;
|
|
17
|
+
method: any;
|
|
18
|
+
url: any;
|
|
19
|
+
durationMs: any;
|
|
20
|
+
}): boolean;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { Event } from '@outputai/core/sdk/runtime';
|
|
2
|
+
export const emitError = ({ requestId, method, url, status, durationMs }) => Event.emit('http:request', { requestId, method, url, status, durationMs, outcome: 'error' });
|
|
3
|
+
export const emitSuccess = ({ requestId, method, url, status, durationMs }) => Event.emit('http:request', { requestId, method, url, status, durationMs, outcome: 'success' });
|
|
4
|
+
export const emitFailure = ({ requestId, method, url, durationMs }) => Event.emit('http:request', { requestId, method, url, status: undefined, durationMs, outcome: 'failure' });
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
vi.mock('@outputai/core/sdk/runtime', () => ({
|
|
3
|
+
Event: {
|
|
4
|
+
emit: vi.fn()
|
|
5
|
+
}
|
|
6
|
+
}));
|
|
7
|
+
import { Event } from '@outputai/core/sdk/runtime';
|
|
8
|
+
import { emitError, emitFailure, emitSuccess } from './events.js';
|
|
9
|
+
const event = vi.mocked(Event, true);
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
event.emit.mockClear();
|
|
12
|
+
});
|
|
13
|
+
describe('instrumented_fetch/events', () => {
|
|
14
|
+
it('emits a successful request', async () => {
|
|
15
|
+
await emitSuccess({
|
|
16
|
+
requestId: 'request-success',
|
|
17
|
+
method: 'GET',
|
|
18
|
+
url: 'https://example.com/success',
|
|
19
|
+
status: 200,
|
|
20
|
+
durationMs: 12
|
|
21
|
+
});
|
|
22
|
+
expect(event.emit).toHaveBeenCalledWith('http:request', {
|
|
23
|
+
requestId: 'request-success',
|
|
24
|
+
method: 'GET',
|
|
25
|
+
url: 'https://example.com/success',
|
|
26
|
+
status: 200,
|
|
27
|
+
durationMs: 12,
|
|
28
|
+
outcome: 'success'
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
it('emits an HTTP error', async () => {
|
|
32
|
+
await emitError({
|
|
33
|
+
requestId: 'request-error',
|
|
34
|
+
method: 'POST',
|
|
35
|
+
url: 'https://example.com/error',
|
|
36
|
+
status: 503,
|
|
37
|
+
durationMs: 23
|
|
38
|
+
});
|
|
39
|
+
expect(event.emit).toHaveBeenCalledWith('http:request', {
|
|
40
|
+
requestId: 'request-error',
|
|
41
|
+
method: 'POST',
|
|
42
|
+
url: 'https://example.com/error',
|
|
43
|
+
status: 503,
|
|
44
|
+
durationMs: 23,
|
|
45
|
+
outcome: 'error'
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
it('emits a transport failure without a status', () => {
|
|
49
|
+
emitFailure({
|
|
50
|
+
requestId: 'request-failure',
|
|
51
|
+
method: 'DELETE',
|
|
52
|
+
url: 'https://example.com/failure',
|
|
53
|
+
durationMs: 34
|
|
54
|
+
});
|
|
55
|
+
expect(event.emit).toHaveBeenCalledWith('http:request', {
|
|
56
|
+
requestId: 'request-failure',
|
|
57
|
+
method: 'DELETE',
|
|
58
|
+
url: 'https://example.com/failure',
|
|
59
|
+
status: undefined,
|
|
60
|
+
durationMs: 34,
|
|
61
|
+
outcome: 'failure'
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
});
|
package/dist/fetch/index.d.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
/** Undici's fetch second argument: A plain object containing HTTP options. */
|
|
9
|
-
RequestInit };
|
|
1
|
+
import * as undici from 'undici';
|
|
2
|
+
type NodeRequestInfo = string | URL | globalThis.Request;
|
|
3
|
+
type NodeRequestInit = globalThis.RequestInit & Pick<undici.RequestInit, 'dispatcher'>;
|
|
4
|
+
type OutputFetch = {
|
|
5
|
+
(input: NodeRequestInfo, init?: NodeRequestInit): Promise<Response>;
|
|
6
|
+
(input: undici.RequestInfo, init?: undici.RequestInit): Promise<Response>;
|
|
7
|
+
};
|
|
10
8
|
/**
|
|
11
9
|
* A fetch compliant function, that wraps undici's fetch.
|
|
12
10
|
*
|
|
13
11
|
* Behaves the same as any fetch function except:
|
|
14
|
-
* - Sets a request header called `x-request
|
|
12
|
+
* - Sets a request header called `x-request-trace-id` with a random UUID;
|
|
15
13
|
* - Sends the request, response, error and/or failure to the Trace system;
|
|
16
14
|
* - Emits a `http:request` event on every call (success, error, failure).
|
|
17
15
|
*
|
|
@@ -20,4 +18,5 @@ RequestInit };
|
|
|
20
18
|
* @param init - Request options
|
|
21
19
|
* @returns The HTTP response
|
|
22
20
|
*/
|
|
23
|
-
export declare const
|
|
21
|
+
export declare const outputFetch: OutputFetch;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const undiciMock = vi.hoisted(() => ({
|
|
3
|
+
dispatcher: { dispatch: vi.fn() },
|
|
4
|
+
envHttpProxyAgent: vi.fn(),
|
|
5
|
+
fetch: vi.fn()
|
|
6
|
+
}));
|
|
7
|
+
vi.mock('undici', async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal();
|
|
9
|
+
class EnvHttpProxyAgent {
|
|
10
|
+
constructor(options) {
|
|
11
|
+
undiciMock.envHttpProxyAgent(options);
|
|
12
|
+
return undiciMock.dispatcher;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
...actual,
|
|
17
|
+
EnvHttpProxyAgent,
|
|
18
|
+
fetch: undiciMock.fetch
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
vi.mock('./logger.js', () => ({
|
|
22
|
+
logRequest: vi.fn(),
|
|
23
|
+
logResponse: vi.fn(),
|
|
24
|
+
logError: vi.fn(),
|
|
25
|
+
logFailure: vi.fn()
|
|
26
|
+
}));
|
|
27
|
+
vi.mock('./events.js', () => ({
|
|
28
|
+
emitSuccess: vi.fn(),
|
|
29
|
+
emitError: vi.fn(),
|
|
30
|
+
emitFailure: vi.fn()
|
|
31
|
+
}));
|
|
32
|
+
vi.mock('./utils.js', () => ({
|
|
33
|
+
addRequestIdToResponse: vi.fn()
|
|
34
|
+
}));
|
|
35
|
+
import { Request, Response } from 'undici';
|
|
36
|
+
import { outputFetch } from './index.js';
|
|
37
|
+
describe('outputFetch default dispatcher', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
undiciMock.fetch.mockReset();
|
|
40
|
+
undiciMock.fetch.mockResolvedValue(new Response('ok'));
|
|
41
|
+
});
|
|
42
|
+
it('uses an EnvHttpProxyAgent when dispatcher is omitted', async () => {
|
|
43
|
+
await outputFetch('https://example.com');
|
|
44
|
+
expect(undiciMock.envHttpProxyAgent).toHaveBeenCalledWith({ allowH2: false });
|
|
45
|
+
expect(undiciMock.fetch).toHaveBeenCalledWith(expect.any(Request), { dispatcher: undiciMock.dispatcher });
|
|
46
|
+
});
|
|
47
|
+
});
|
package/dist/fetch/index.js
CHANGED
|
@@ -1,21 +1,29 @@
|
|
|
1
|
-
import * as undici from 'undici';
|
|
2
1
|
import { randomUUID } from 'node:crypto';
|
|
3
2
|
import { logRequest, logResponse, logError, logFailure } from './logger.js';
|
|
3
|
+
import { emitSuccess, emitError, emitFailure } from './events.js';
|
|
4
4
|
import { addRequestIdToResponse } from './utils.js';
|
|
5
|
+
import * as undici from 'undici';
|
|
5
6
|
/* Ignore HTTP/2. Check: https://github.com/growthxai/output/issues/299 */
|
|
6
7
|
const customDispatcher = new undici.EnvHttpProxyAgent({ allowH2: false });
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
undici.
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
const createUndiciRequest = (input, init) => {
|
|
9
|
+
const isNodeRequest = input instanceof globalThis.Request;
|
|
10
|
+
const hasNodeFormData = init?.body instanceof globalThis.FormData;
|
|
11
|
+
const isUndiciRequest = input instanceof undici.Request;
|
|
12
|
+
const hasUndiciFormData = init?.body instanceof undici.FormData;
|
|
13
|
+
if ((isNodeRequest && hasUndiciFormData) || (isUndiciRequest && hasNodeFormData)) {
|
|
14
|
+
throw new TypeError('Cannot mix Node and Undici Request/FormData realms.');
|
|
15
|
+
}
|
|
16
|
+
if (!isNodeRequest && !hasNodeFormData) {
|
|
17
|
+
return new undici.Request(input, init);
|
|
18
|
+
}
|
|
19
|
+
const request = new globalThis.Request(input, init);
|
|
20
|
+
return new undici.Request(request.url, request);
|
|
21
|
+
};
|
|
14
22
|
/**
|
|
15
23
|
* A fetch compliant function, that wraps undici's fetch.
|
|
16
24
|
*
|
|
17
25
|
* Behaves the same as any fetch function except:
|
|
18
|
-
* - Sets a request header called `x-request
|
|
26
|
+
* - Sets a request header called `x-request-trace-id` with a random UUID;
|
|
19
27
|
* - Sends the request, response, error and/or failure to the Trace system;
|
|
20
28
|
* - Emits a `http:request` event on every call (success, error, failure).
|
|
21
29
|
*
|
|
@@ -24,9 +32,10 @@ export * as undici from 'undici';
|
|
|
24
32
|
* @param init - Request options
|
|
25
33
|
* @returns The HTTP response
|
|
26
34
|
*/
|
|
27
|
-
export const
|
|
35
|
+
export const outputFetch = async (input, init) => {
|
|
36
|
+
const { dispatcher: inputDispatcher, ...requestInit } = (init ?? {});
|
|
28
37
|
// Creates a Request object with the many shapes RequestInfo can have
|
|
29
|
-
const base =
|
|
38
|
+
const base = createUndiciRequest(input, requestInit);
|
|
30
39
|
// Creates a headers object with the many shapes Request.Headers can have (object, array, Headers)
|
|
31
40
|
const headers = new undici.Headers(base.headers);
|
|
32
41
|
const requestId = randomUUID();
|
|
@@ -34,25 +43,29 @@ export const fetch = async (input, init) => {
|
|
|
34
43
|
const request = new undici.Request(base, { headers });
|
|
35
44
|
const method = request.method;
|
|
36
45
|
const url = request.url;
|
|
37
|
-
const startedAt =
|
|
46
|
+
const startedAt = Date.now();
|
|
38
47
|
await logRequest({ requestId, request });
|
|
39
48
|
// this allows for users not only to override the dispatcher but also to define it as undefined and remove it altogether.
|
|
40
|
-
const dispatcher = Object.hasOwn(init ?? {}, 'dispatcher') ?
|
|
49
|
+
const dispatcher = Object.hasOwn(init ?? {}, 'dispatcher') ? inputDispatcher : customDispatcher;
|
|
41
50
|
try {
|
|
42
51
|
const response = await undici.fetch(request, dispatcher ? { dispatcher } : undefined);
|
|
43
|
-
const durationMs =
|
|
52
|
+
const durationMs = Date.now() - startedAt;
|
|
53
|
+
const { status } = response;
|
|
44
54
|
// This enriches the response of the request id, so it is identifiable later.
|
|
45
55
|
addRequestIdToResponse(response, requestId);
|
|
46
|
-
if (
|
|
47
|
-
await logError({ requestId, response
|
|
56
|
+
if (status > 399) {
|
|
57
|
+
await logError({ requestId, response });
|
|
58
|
+
emitError({ requestId, method, url, status, durationMs });
|
|
48
59
|
return response;
|
|
49
60
|
}
|
|
50
|
-
await logResponse({ requestId, response
|
|
61
|
+
await logResponse({ requestId, response });
|
|
62
|
+
emitSuccess({ requestId, method, url, status, durationMs });
|
|
51
63
|
return response;
|
|
52
64
|
}
|
|
53
65
|
catch (error) {
|
|
54
|
-
const durationMs =
|
|
55
|
-
logFailure({ requestId, error
|
|
66
|
+
const durationMs = Date.now() - startedAt;
|
|
67
|
+
logFailure({ requestId, error });
|
|
68
|
+
emitFailure({ requestId, method, url, durationMs });
|
|
56
69
|
throw error;
|
|
57
70
|
}
|
|
58
71
|
};
|
package/dist/fetch/index.spec.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { getGlobalDispatcher, Headers, MockAgent, Request, setGlobalDispatcher } from 'undici';
|
|
2
|
+
import { FormData as UndiciFormData, getGlobalDispatcher, Headers, MockAgent, Request, setGlobalDispatcher } from 'undici';
|
|
3
3
|
const FIXED_REQUEST_ID = 'aaaaaaaa-bbbb-4ccc-dddd-eeeeeeeeeeee';
|
|
4
4
|
const randomUUIDMock = vi.hoisted(() => vi.fn(() => FIXED_REQUEST_ID));
|
|
5
5
|
const loggerMock = vi.hoisted(() => ({
|
|
@@ -11,6 +11,11 @@ const loggerMock = vi.hoisted(() => ({
|
|
|
11
11
|
const utilsMock = vi.hoisted(() => ({
|
|
12
12
|
addRequestIdToResponse: vi.fn()
|
|
13
13
|
}));
|
|
14
|
+
const eventsMock = vi.hoisted(() => ({
|
|
15
|
+
emitSuccess: vi.fn(),
|
|
16
|
+
emitError: vi.fn(),
|
|
17
|
+
emitFailure: vi.fn()
|
|
18
|
+
}));
|
|
14
19
|
vi.mock('node:crypto', () => ({
|
|
15
20
|
randomUUID: () => randomUUIDMock()
|
|
16
21
|
}));
|
|
@@ -23,14 +28,19 @@ vi.mock('./logger.js', () => ({
|
|
|
23
28
|
vi.mock('./utils.js', () => ({
|
|
24
29
|
addRequestIdToResponse: utilsMock.addRequestIdToResponse
|
|
25
30
|
}));
|
|
26
|
-
|
|
31
|
+
vi.mock('./events.js', () => ({
|
|
32
|
+
emitSuccess: eventsMock.emitSuccess,
|
|
33
|
+
emitError: eventsMock.emitError,
|
|
34
|
+
emitFailure: eventsMock.emitFailure
|
|
35
|
+
}));
|
|
36
|
+
import { outputFetch } from './index.js';
|
|
27
37
|
const MOCK_ORIGIN = 'https://fetch-index.undici.test';
|
|
28
|
-
describe('
|
|
38
|
+
describe('instrumented_fetch/index', () => {
|
|
29
39
|
const undiciCtx = {
|
|
30
40
|
mockAgent: undefined,
|
|
31
41
|
previousDispatcher: undefined
|
|
32
42
|
};
|
|
33
|
-
const fetchWithMockDispatcher = (input, init) =>
|
|
43
|
+
const fetchWithMockDispatcher = (input, init) => outputFetch(input, { ...init, dispatcher: undiciCtx.mockAgent });
|
|
34
44
|
beforeEach(() => {
|
|
35
45
|
undiciCtx.mockAgent = new MockAgent();
|
|
36
46
|
undiciCtx.mockAgent.disableNetConnect();
|
|
@@ -41,6 +51,9 @@ describe('fetch/index', () => {
|
|
|
41
51
|
loggerMock.logError.mockClear();
|
|
42
52
|
loggerMock.logFailure.mockClear();
|
|
43
53
|
utilsMock.addRequestIdToResponse.mockClear();
|
|
54
|
+
eventsMock.emitSuccess.mockClear();
|
|
55
|
+
eventsMock.emitError.mockClear();
|
|
56
|
+
eventsMock.emitFailure.mockClear();
|
|
44
57
|
randomUUIDMock.mockClear();
|
|
45
58
|
randomUUIDMock.mockImplementation(() => FIXED_REQUEST_ID);
|
|
46
59
|
});
|
|
@@ -66,14 +79,17 @@ describe('fetch/index', () => {
|
|
|
66
79
|
const responseCall = loggerMock.logResponse.mock.calls[0][0];
|
|
67
80
|
expect(responseCall.requestId).toBe(FIXED_REQUEST_ID);
|
|
68
81
|
expect(responseCall.response).toBe(response);
|
|
69
|
-
expect(responseCall.method).toBe('GET');
|
|
70
|
-
expect(responseCall.url).toBe(`${MOCK_ORIGIN}/ok`);
|
|
71
|
-
expect(typeof responseCall.durationMs).toBe('number');
|
|
72
|
-
expect(responseCall.durationMs).toBeGreaterThanOrEqual(0);
|
|
73
82
|
expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledTimes(1);
|
|
74
83
|
expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledWith(response, FIXED_REQUEST_ID);
|
|
75
84
|
expect(loggerMock.logError).not.toHaveBeenCalled();
|
|
76
85
|
expect(loggerMock.logFailure).not.toHaveBeenCalled();
|
|
86
|
+
expect(eventsMock.emitSuccess).toHaveBeenCalledWith({
|
|
87
|
+
requestId: FIXED_REQUEST_ID,
|
|
88
|
+
method: 'GET',
|
|
89
|
+
url: `${MOCK_ORIGIN}/ok`,
|
|
90
|
+
status: 200,
|
|
91
|
+
durationMs: expect.any(Number)
|
|
92
|
+
});
|
|
77
93
|
});
|
|
78
94
|
it('uses logError for status > 399 without logging response end', async () => {
|
|
79
95
|
undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/missing', method: 'GET' }).reply(404, 'nope', { headers: { 'content-type': 'text/plain' } });
|
|
@@ -86,6 +102,13 @@ describe('fetch/index', () => {
|
|
|
86
102
|
expect(loggerMock.logError.mock.calls[0][0].response).toBe(response);
|
|
87
103
|
expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledTimes(1);
|
|
88
104
|
expect(utilsMock.addRequestIdToResponse).toHaveBeenCalledWith(response, FIXED_REQUEST_ID);
|
|
105
|
+
expect(eventsMock.emitError).toHaveBeenCalledWith({
|
|
106
|
+
requestId: FIXED_REQUEST_ID,
|
|
107
|
+
method: 'GET',
|
|
108
|
+
url: `${MOCK_ORIGIN}/missing`,
|
|
109
|
+
status: 404,
|
|
110
|
+
durationMs: expect.any(Number)
|
|
111
|
+
});
|
|
89
112
|
});
|
|
90
113
|
it('treats status 399 as success (logs response end, not HTTP error)', async () => {
|
|
91
114
|
undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/edge', method: 'GET' }).reply(399, '', { headers: { 'content-type': 'text/plain' } });
|
|
@@ -142,6 +165,12 @@ describe('fetch/index', () => {
|
|
|
142
165
|
expect(failure.message).toBe('fetch failed');
|
|
143
166
|
expect(failure.cause).toBeInstanceOf(Error);
|
|
144
167
|
expect(failure.cause.message).toBe('simulated network failure');
|
|
168
|
+
expect(eventsMock.emitFailure).toHaveBeenCalledWith({
|
|
169
|
+
requestId: FIXED_REQUEST_ID,
|
|
170
|
+
method: 'GET',
|
|
171
|
+
url: `${MOCK_ORIGIN}/boom`,
|
|
172
|
+
durationMs: expect.any(Number)
|
|
173
|
+
});
|
|
145
174
|
});
|
|
146
175
|
it('fails when no mock matches (disabled net)', async () => {
|
|
147
176
|
await expect(fetchWithMockDispatcher(`${MOCK_ORIGIN}/unmocked`)).rejects.toThrow();
|
|
@@ -168,7 +197,7 @@ describe('fetch/index', () => {
|
|
|
168
197
|
expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('GET');
|
|
169
198
|
});
|
|
170
199
|
});
|
|
171
|
-
describe('
|
|
200
|
+
describe('outputFetch RequestInfo / RequestInit shapes', () => {
|
|
172
201
|
it('accepts a URL object as the first argument', async () => {
|
|
173
202
|
undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/from-url', method: 'GET' }).reply(200, 'url-ok', { headers: { 'content-type': 'text/plain' } });
|
|
174
203
|
const href = new URL('/from-url', `${MOCK_ORIGIN}/`);
|
|
@@ -187,6 +216,80 @@ describe('fetch/index', () => {
|
|
|
187
216
|
expect(request.method).toBe('GET');
|
|
188
217
|
expect(request.url).toBe(`${MOCK_ORIGIN}/req-only`);
|
|
189
218
|
});
|
|
219
|
+
it('accepts Node built-in Request and Headers instances', async () => {
|
|
220
|
+
undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({
|
|
221
|
+
path: '/node-request',
|
|
222
|
+
method: 'POST',
|
|
223
|
+
headers: {
|
|
224
|
+
'content-type': 'application/json',
|
|
225
|
+
'x-node-header': 'node-value',
|
|
226
|
+
'x-request-trace-id': FIXED_REQUEST_ID
|
|
227
|
+
}
|
|
228
|
+
}).reply(201, JSON.stringify({ created: true }), {
|
|
229
|
+
headers: { 'content-type': 'application/json' }
|
|
230
|
+
});
|
|
231
|
+
const headers = new globalThis.Headers({
|
|
232
|
+
'content-type': 'application/json',
|
|
233
|
+
'x-node-header': 'node-value'
|
|
234
|
+
});
|
|
235
|
+
const input = new globalThis.Request(`${MOCK_ORIGIN}/node-request`, {
|
|
236
|
+
method: 'POST',
|
|
237
|
+
headers,
|
|
238
|
+
body: JSON.stringify({ name: 'node' })
|
|
239
|
+
});
|
|
240
|
+
const response = await fetchWithMockDispatcher(input);
|
|
241
|
+
const nodeResponse = response;
|
|
242
|
+
expect(input).not.toBeInstanceOf(Request);
|
|
243
|
+
expect(nodeResponse.status).toBe(201);
|
|
244
|
+
await expect(nodeResponse.json()).resolves.toEqual({ created: true });
|
|
245
|
+
expect(loggerMock.logRequest.mock.calls[0][0].request).toBeInstanceOf(Request);
|
|
246
|
+
expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('POST');
|
|
247
|
+
});
|
|
248
|
+
it('encodes Node built-in FormData before entering the Undici realm', async () => {
|
|
249
|
+
undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({
|
|
250
|
+
path: '/node-form-data',
|
|
251
|
+
method: 'POST'
|
|
252
|
+
}).reply(200, 'ok');
|
|
253
|
+
const body = new globalThis.FormData();
|
|
254
|
+
body.set('name', 'node');
|
|
255
|
+
loggerMock.logRequest.mockImplementationOnce(async ({ request }) => {
|
|
256
|
+
expect(request).toBeInstanceOf(Request);
|
|
257
|
+
expect(request.headers.get('content-type')).toMatch(/^multipart\/form-data; boundary=/);
|
|
258
|
+
await expect(request.clone().text()).resolves.toContain('name="name"\r\n\r\nnode');
|
|
259
|
+
});
|
|
260
|
+
const response = await fetchWithMockDispatcher(`${MOCK_ORIGIN}/node-form-data`, {
|
|
261
|
+
method: 'POST',
|
|
262
|
+
body
|
|
263
|
+
});
|
|
264
|
+
expect(response.status).toBe(200);
|
|
265
|
+
});
|
|
266
|
+
it('rejects mixed Request and FormData realms', async () => {
|
|
267
|
+
const input = new globalThis.Request(`${MOCK_ORIGIN}/mixed-realms`, { method: 'POST', body: 'node' });
|
|
268
|
+
const body = new UndiciFormData();
|
|
269
|
+
body.set('name', 'undici');
|
|
270
|
+
await expect(fetchWithMockDispatcher(input, {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
body: body
|
|
273
|
+
})).rejects.toThrow('Cannot mix Node and Undici Request/FormData realms.');
|
|
274
|
+
});
|
|
275
|
+
it('accepts Node built-in RequestInit with a string input', async () => {
|
|
276
|
+
undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({
|
|
277
|
+
path: '/node-init',
|
|
278
|
+
method: 'PATCH',
|
|
279
|
+
headers: {
|
|
280
|
+
'x-node-init': 'yes',
|
|
281
|
+
'x-request-trace-id': FIXED_REQUEST_ID
|
|
282
|
+
}
|
|
283
|
+
}).reply(204);
|
|
284
|
+
const init = {
|
|
285
|
+
method: 'PATCH',
|
|
286
|
+
headers: new globalThis.Headers({ 'x-node-init': 'yes' })
|
|
287
|
+
};
|
|
288
|
+
const response = await fetchWithMockDispatcher(`${MOCK_ORIGIN}/node-init`, init);
|
|
289
|
+
expect(response.status).toBe(204);
|
|
290
|
+
expect(loggerMock.logRequest.mock.calls[0][0].request.method).toBe('PATCH');
|
|
291
|
+
expect(loggerMock.logRequest.mock.calls[0][0].request.headers.get('x-node-init')).toBe('yes');
|
|
292
|
+
});
|
|
190
293
|
it('accepts Request plus init that overrides method and body', async () => {
|
|
191
294
|
undiciCtx.mockAgent.get(MOCK_ORIGIN).intercept({ path: '/req-plus-init', method: 'POST' }).reply(201, JSON.stringify({ saved: true }), { headers: { 'content-type': 'application/json' } });
|
|
192
295
|
const input = new Request(`${MOCK_ORIGIN}/req-plus-init`, { method: 'GET' });
|
|
@@ -232,7 +335,7 @@ describe('fetch/index', () => {
|
|
|
232
335
|
'x-custom': 'custom-value'
|
|
233
336
|
}
|
|
234
337
|
}).reply(200, 'ok');
|
|
235
|
-
const response = await
|
|
338
|
+
const response = await outputFetch(`${MOCK_ORIGIN}/custom-dispatcher`, {
|
|
236
339
|
method: 'POST',
|
|
237
340
|
headers: {
|
|
238
341
|
'Content-Type': 'application/json',
|
|
@@ -253,7 +356,7 @@ describe('fetch/index', () => {
|
|
|
253
356
|
method: 'GET',
|
|
254
357
|
headers: { 'x-request-trace-id': FIXED_REQUEST_ID }
|
|
255
358
|
}).reply(200, 'ok');
|
|
256
|
-
const response = await
|
|
359
|
+
const response = await outputFetch(`${MOCK_ORIGIN}/undefined-dispatcher`, {
|
|
257
360
|
dispatcher: undefined
|
|
258
361
|
});
|
|
259
362
|
expect(response.status).toBe(200);
|
package/dist/fetch/logger.d.ts
CHANGED
|
@@ -1,66 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
requestId:
|
|
11
|
-
|
|
12
|
-
})
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* @param options
|
|
18
|
-
* @param options.requestId - id of the request
|
|
19
|
-
* @param options.response - The HTTP Response object
|
|
20
|
-
* @param options.method - HTTP method of the request
|
|
21
|
-
* @param options.url - URL of the request
|
|
22
|
-
* @param options.durationMs - elapsed time from request issuance to response, in milliseconds
|
|
23
|
-
*/
|
|
24
|
-
export declare const logError: ({ requestId, response, method, url, durationMs }: {
|
|
25
|
-
requestId: string;
|
|
26
|
-
response: Response;
|
|
27
|
-
method: string;
|
|
28
|
-
url: string;
|
|
29
|
-
durationMs: number;
|
|
30
|
-
}) => Promise<void>;
|
|
31
|
-
/**
|
|
32
|
-
* Sends the trace end event for an http response
|
|
33
|
-
* and emits a `http:request` event with `outcome: 'success'`.
|
|
34
|
-
*
|
|
35
|
-
* @param {object} options
|
|
36
|
-
* @param options.requestId - id of the request
|
|
37
|
-
* @param {Response} options.response - The HTTP Response object
|
|
38
|
-
* @param options.method - HTTP method of the request
|
|
39
|
-
* @param options.url - URL of the request
|
|
40
|
-
* @param options.durationMs - elapsed time from request issuance to response, in milliseconds
|
|
41
|
-
*/
|
|
42
|
-
export declare const logResponse: ({ requestId, response, method, url, durationMs }: {
|
|
43
|
-
requestId: string;
|
|
44
|
-
response: Response;
|
|
45
|
-
method: string;
|
|
46
|
-
url: string;
|
|
47
|
-
durationMs: number;
|
|
48
|
-
}) => Promise<void>;
|
|
49
|
-
/**
|
|
50
|
-
* Creates the trace error event for a network/connection failure
|
|
51
|
-
* and emits a `http:request` event with `outcome: 'failure'`.
|
|
52
|
-
*
|
|
53
|
-
* @param options
|
|
54
|
-
* @param options.requestId - id of the request
|
|
55
|
-
* @param options.error - The error thrown
|
|
56
|
-
* @param options.method - HTTP method of the request
|
|
57
|
-
* @param options.url - URL of the request
|
|
58
|
-
* @param options.durationMs - elapsed time from request issuance to failure, in milliseconds
|
|
59
|
-
*/
|
|
60
|
-
export declare const logFailure: ({ requestId, error, method, url, durationMs }: {
|
|
61
|
-
requestId: string;
|
|
62
|
-
error: Error;
|
|
63
|
-
method: string;
|
|
64
|
-
url: string;
|
|
65
|
-
durationMs: number;
|
|
66
|
-
}) => void;
|
|
1
|
+
export function logRequest({ requestId, request }: {
|
|
2
|
+
requestId: any;
|
|
3
|
+
request: any;
|
|
4
|
+
}): Promise<void>;
|
|
5
|
+
export function logError({ requestId, response }: {
|
|
6
|
+
requestId: any;
|
|
7
|
+
response: any;
|
|
8
|
+
}): Promise<void>;
|
|
9
|
+
export function logResponse({ requestId, response }: {
|
|
10
|
+
requestId: any;
|
|
11
|
+
response: any;
|
|
12
|
+
}): Promise<void>;
|
|
13
|
+
export function logFailure({ requestId, error }: {
|
|
14
|
+
requestId: any;
|
|
15
|
+
error: any;
|
|
16
|
+
}): void;
|
package/dist/fetch/logger.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { Tracing
|
|
1
|
+
import { Tracing } from '@outputai/core/sdk/runtime';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
3
|
import { parseBody, redactHeaders, serializeError } from './utils.js';
|
|
4
|
-
/** Single source of truth for the `http:request` event shape. */
|
|
5
|
-
const emitHttpRequestEvent = (payload) => {
|
|
6
|
-
Event.emit('http:request', payload);
|
|
7
|
-
};
|
|
8
4
|
/**
|
|
9
5
|
* Sends the trace start event for an http request
|
|
10
6
|
*
|
|
@@ -22,67 +18,18 @@ export const logRequest = async ({ requestId, request }) => {
|
|
|
22
18
|
});
|
|
23
19
|
Tracing.addEventAttribute({ eventId: requestId, attribute: new Tracing.Attribute.HTTPRequestCount(request.url, requestId) });
|
|
24
20
|
};
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
statusText: response.statusText,
|
|
41
|
-
headers: redactHeaders(response.headers),
|
|
42
|
-
body: await parseBody(response)
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
emitHttpRequestEvent({
|
|
46
|
-
requestId, method, url, status: response.status, durationMs, outcome: 'error'
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Sends the trace end event for an http response
|
|
51
|
-
* and emits a `http:request` event with `outcome: 'success'`.
|
|
52
|
-
*
|
|
53
|
-
* @param {object} options
|
|
54
|
-
* @param options.requestId - id of the request
|
|
55
|
-
* @param {Response} options.response - The HTTP Response object
|
|
56
|
-
* @param options.method - HTTP method of the request
|
|
57
|
-
* @param options.url - URL of the request
|
|
58
|
-
* @param options.durationMs - elapsed time from request issuance to response, in milliseconds
|
|
59
|
-
*/
|
|
60
|
-
export const logResponse = async ({ requestId, response, method, url, durationMs }) => {
|
|
61
|
-
await Tracing.addEventEnd({
|
|
62
|
-
id: requestId, details: {
|
|
63
|
-
status: response.status,
|
|
64
|
-
statusText: response.statusText,
|
|
65
|
-
...(config.logVerbose && { headers: redactHeaders(response.headers), body: await parseBody(response) })
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
emitHttpRequestEvent({
|
|
69
|
-
requestId, method, url, status: response.status, durationMs, outcome: 'success'
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
/**
|
|
73
|
-
* Creates the trace error event for a network/connection failure
|
|
74
|
-
* and emits a `http:request` event with `outcome: 'failure'`.
|
|
75
|
-
*
|
|
76
|
-
* @param options
|
|
77
|
-
* @param options.requestId - id of the request
|
|
78
|
-
* @param options.error - The error thrown
|
|
79
|
-
* @param options.method - HTTP method of the request
|
|
80
|
-
* @param options.url - URL of the request
|
|
81
|
-
* @param options.durationMs - elapsed time from request issuance to failure, in milliseconds
|
|
82
|
-
*/
|
|
83
|
-
export const logFailure = ({ requestId, error, method, url, durationMs }) => {
|
|
84
|
-
Tracing.addEventError({ id: requestId, details: serializeError(error) });
|
|
85
|
-
emitHttpRequestEvent({
|
|
86
|
-
requestId, method, url, status: undefined, durationMs, outcome: 'failure'
|
|
87
|
-
});
|
|
88
|
-
};
|
|
21
|
+
export const logError = async ({ requestId, response }) => Tracing.addEventError({
|
|
22
|
+
id: requestId, details: {
|
|
23
|
+
status: response.status,
|
|
24
|
+
statusText: response.statusText,
|
|
25
|
+
...(config.logVerbose && { headers: redactHeaders(response.headers), body: await parseBody(response) })
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
export const logResponse = async ({ requestId, response }) => Tracing.addEventEnd({
|
|
29
|
+
id: requestId, details: {
|
|
30
|
+
status: response.status,
|
|
31
|
+
statusText: response.statusText,
|
|
32
|
+
...(config.logVerbose && { headers: redactHeaders(response.headers), body: await parseBody(response) })
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
export const logFailure = ({ requestId, error }) => Tracing.addEventError({ id: requestId, details: serializeError(error) });
|