From e93ea16acc4e69807b68300ff1105013ca880beb Mon Sep 17 00:00:00 2001 From: "d.kholstinin" Date: Mon, 8 Dec 2025 16:34:39 +0500 Subject: [PATCH 1/2] feat: warning when wrong http agent used --- .../plugin-protocol-http/src/http.spec.ts | 62 +++++++++++++++---- packages/plugin-protocol-http/src/http.ts | 19 +++++- 2 files changed, 68 insertions(+), 13 deletions(-) diff --git a/packages/plugin-protocol-http/src/http.spec.ts b/packages/plugin-protocol-http/src/http.spec.ts index be4f9a3..f4240ec 100644 --- a/packages/plugin-protocol-http/src/http.spec.ts +++ b/packages/plugin-protocol-http/src/http.spec.ts @@ -9,6 +9,8 @@ jest.mock('undici', () => { import { Context, Status } from '@tinkoff/request-core'; import http from './http'; +import https from 'https'; +import { Agent } from 'undici'; const plugin = http(); const next = jest.fn(); @@ -162,13 +164,9 @@ describe('plugins/http', () => { }); (fetch as any).mockReturnValueOnce(mockResponse); - class MockedAgent { - dispatch() {} - } + const mockedAgent = new Agent(); - const mockedAgent = new MockedAgent(); - - http({ agent: { http: mockedAgent as any, https: undefined as any } }).init?.( + http({ agent: { http: mockedAgent, https: undefined as any } }).init?.( new Context({ request: { url: 'http://test.com/api', @@ -202,6 +200,50 @@ describe('plugins/http', () => { }); }); + it('request with wrong https agent', async () => { + const body = { a: 3 }; + const mockResponse = createResponse(body, { + headers: { + 'Content-type': 'application/json;', + }, + }); + (fetch as any).mockReturnValueOnce(mockResponse); + + const mockedAgent = new https.Agent(); + + http({ agent: { http: undefined as any, https: mockedAgent as any } }).init?.( + new Context({ + request: { + url: 'https://test.com/api', + headers: { + 'Content-type': 'application/json', + }, + }, + }), + next, + null as any + ); + + await new Promise((res) => { + next.mockImplementation(res); + }); + + expect(fetch as any).toHaveBeenCalledWith('https://test.com/api', { + method: 'GET', + credentials: 'same-origin', + body: undefined, + headers: { + 'Content-type': 'application/json', + }, + signal: expect.anything(), + }); + + expect(next).toHaveBeenLastCalledWith({ + response: body, + status: Status.COMPLETE, + }); + }); + it('request with custom https agent', async () => { const body = { a: 3 }; const mockResponse = createResponse(body, { @@ -211,13 +253,9 @@ describe('plugins/http', () => { }); (fetch as any).mockReturnValueOnce(mockResponse); - class MockedAgent { - dispatch() {} - } + const mockedAgent = new Agent(); - const mockedAgent = new MockedAgent(); - - http({ agent: { http: undefined as any, https: mockedAgent as any } }).init?.( + http({ agent: { http: undefined as any, https: mockedAgent } }).init?.( new Context({ request: { url: 'https://test.com/api', diff --git a/packages/plugin-protocol-http/src/http.ts b/packages/plugin-protocol-http/src/http.ts index c98c74c..23b4a2d 100644 --- a/packages/plugin-protocol-http/src/http.ts +++ b/packages/plugin-protocol-http/src/http.ts @@ -9,7 +9,9 @@ import { PROTOCOL_HTTP, REQUEST_TYPES, HttpMethods } from './constants'; import parse from './parse'; import createForm from './form'; import { TimeoutError, AbortError, HttpRequestError } from './errors'; -import type { Agent } from 'undici/types'; +import { Agent } from 'undici'; +import type { Agent as HttpAgent } from 'http'; +import type { Agent as HttpsAgent } from 'https'; declare module '@tinkoff/request-core/lib/types.h' { export interface Request { @@ -49,6 +51,11 @@ declare module '@tinkoff/request-core/lib/types.h' { } const isBrowser = typeof window !== 'undefined'; +const getWarnMessage = (agentType: 'http' | 'https') => `Provided ${agentType} agent is not an instance of undici, so it will be ignored`; + +function isProvidedAgentUndiciInstance(agent: HttpAgent | HttpsAgent | Agent): boolean { + return agent instanceof Agent; +} /** * Makes http/https request. @@ -86,10 +93,20 @@ export default ({ const parsedUrl = new URL(url); if (parsedUrl.protocol === 'http:') { + if (!isProvidedAgentUndiciInstance(agent.http)) { + console.warn(getWarnMessage('http')); + return undefined; + } + return agent.http; } } catch (_err) {} + if (!isProvidedAgentUndiciInstance(agent.https)) { + console.warn(getWarnMessage('https')); + return undefined; + } + return agent.https; }; } From feabbc2dcf27725df5e451a5ad9b95610f633427 Mon Sep 17 00:00:00 2001 From: "d.kholstinin" Date: Mon, 8 Dec 2025 17:12:14 +0500 Subject: [PATCH 2/2] fix: http agent type --- packages/plugin-protocol-http/package.json | 2 +- .../plugin-protocol-http/src/fetch.browser.ts | 5 +- packages/plugin-protocol-http/src/fetch.ts | 4 +- .../plugin-protocol-http/src/http.spec.ts | 47 ++++++++++++++++++- packages/plugin-protocol-http/src/http.ts | 29 ++++++------ 5 files changed, 69 insertions(+), 18 deletions(-) diff --git a/packages/plugin-protocol-http/package.json b/packages/plugin-protocol-http/package.json index 2d9d362..761669d 100644 --- a/packages/plugin-protocol-http/package.json +++ b/packages/plugin-protocol-http/package.json @@ -5,7 +5,7 @@ "main": "lib/index.js", "typings": "lib/index.d.ts", "browser": { - "lib/fetch.js": "./lib/fetch.browser.js", + "./lib/fetch.js": "./lib/fetch.browser.js", "./lib/index.es.js": "./lib/index.browser.js" }, "sideEffects": false, diff --git a/packages/plugin-protocol-http/src/fetch.browser.ts b/packages/plugin-protocol-http/src/fetch.browser.ts index caa063e..d2464dc 100644 --- a/packages/plugin-protocol-http/src/fetch.browser.ts +++ b/packages/plugin-protocol-http/src/fetch.browser.ts @@ -23,4 +23,7 @@ const fetch = (...args) => { const { Headers, Request, Response } = glob; -export { fetch, Headers, Request, Response }; +class HttpAgent {} +class HttpsAgent {} + +export { fetch, Headers, Request, Response, HttpAgent, HttpsAgent}; diff --git a/packages/plugin-protocol-http/src/fetch.ts b/packages/plugin-protocol-http/src/fetch.ts index 9940fda..f5ce5e2 100644 --- a/packages/plugin-protocol-http/src/fetch.ts +++ b/packages/plugin-protocol-http/src/fetch.ts @@ -1,4 +1,6 @@ import { RequestInit, RequestInfo, fetch as undiciFetch } from 'undici'; +import { Agent as HttpAgent } from 'http'; +import { Agent as HttpsAgent } from 'https'; const fetch = (input: RequestInfo, init?: RequestInit) => { return undiciFetch(input, init); @@ -6,4 +8,4 @@ const fetch = (input: RequestInfo, init?: RequestInit) => { const { Headers, Request, Response } = globalThis; -export { fetch, Headers, Request, Response }; +export { fetch, Headers, Request, Response, HttpAgent, HttpsAgent }; diff --git a/packages/plugin-protocol-http/src/http.spec.ts b/packages/plugin-protocol-http/src/http.spec.ts index f4240ec..ba3670a 100644 --- a/packages/plugin-protocol-http/src/http.spec.ts +++ b/packages/plugin-protocol-http/src/http.spec.ts @@ -10,7 +10,7 @@ jest.mock('undici', () => { import { Context, Status } from '@tinkoff/request-core'; import http from './http'; import https from 'https'; -import { Agent } from 'undici'; +import { Agent, EnvHttpProxyAgent } from 'undici'; const plugin = http(); const next = jest.fn(); @@ -289,6 +289,51 @@ describe('plugins/http', () => { }); }); + it('request with custom EnvHttpProxyAgent agent', async () => { + const body = { a: 3 }; + const mockResponse = createResponse(body, { + headers: { + 'Content-type': 'application/json', + }, + }); + (fetch as any).mockReturnValueOnce(mockResponse); + + const mockedAgent = new EnvHttpProxyAgent(); + + http({ agent: { http: mockedAgent, https: undefined as any } }).init?.( + new Context({ + request: { + url: 'http://test.com/api', + headers: { + 'Content-type': 'application/json', + }, + }, + }), + next, + null as any + ); + + await new Promise((res) => { + next.mockImplementation(res); + }); + + expect(fetch as any).toHaveBeenCalledWith('http://test.com/api', { + dispatcher: mockedAgent, + method: 'GET', + credentials: 'same-origin', + body: undefined, + headers: { + 'Content-type': 'application/json', + }, + signal: expect.anything(), + }); + + expect(next).toHaveBeenLastCalledWith({ + response: body, + status: Status.COMPLETE, + }); + }); + it('request with custom querySerializer', async () => { const body = { a: 3 }; const mockResponse = createResponse(body, { diff --git a/packages/plugin-protocol-http/src/http.ts b/packages/plugin-protocol-http/src/http.ts index 23b4a2d..90bf2ba 100644 --- a/packages/plugin-protocol-http/src/http.ts +++ b/packages/plugin-protocol-http/src/http.ts @@ -3,15 +3,13 @@ import { Plugin, Status } from '@tinkoff/request-core'; import { Query, QuerySerializer } from '@tinkoff/request-url-utils'; import { addQuery, normalizeUrl } from '@tinkoff/request-url-utils'; -import { fetch } from './fetch'; +import { fetch, HttpAgent, HttpsAgent } from './fetch'; import { serialize } from './serialize'; import { PROTOCOL_HTTP, REQUEST_TYPES, HttpMethods } from './constants'; import parse from './parse'; import createForm from './form'; import { TimeoutError, AbortError, HttpRequestError } from './errors'; -import { Agent } from 'undici'; -import type { Agent as HttpAgent } from 'http'; -import type { Agent as HttpsAgent } from 'https'; +import type { Dispatcher } from 'undici/types'; declare module '@tinkoff/request-core/lib/types.h' { export interface Request { @@ -51,10 +49,13 @@ declare module '@tinkoff/request-core/lib/types.h' { } const isBrowser = typeof window !== 'undefined'; -const getWarnMessage = (agentType: 'http' | 'https') => `Provided ${agentType} agent is not an instance of undici, so it will be ignored`; +const showWarn = (agentType: 'http' | 'https') => + console.warn( + `Starting from 0.15.0 plugin-protocol-http use undici fetch and corresponding Agent\nProvided ${agentType} Agent from node:${agentType} module, so it will be ignored` + ); -function isProvidedAgentUndiciInstance(agent: HttpAgent | HttpsAgent | Agent): boolean { - return agent instanceof Agent; +function isProvidedAgentBelongsNode(agent: HttpAgent | HttpsAgent | Dispatcher): boolean { + return agent instanceof HttpAgent || agent instanceof HttpsAgent; } /** @@ -74,7 +75,7 @@ function isProvidedAgentUndiciInstance(agent: HttpAgent | HttpsAgent | Agent): b * abortPromise {Promise} * signal {AbortSignal} * - * @param {agent} [agent = Agent] set custom agent for fetch in node js. The browser ignores this parameter. + * @param {agent} agent set custom agent for fetch in node js. The browser ignores this parameter. * @param {QuerySerializer} querySerializer function that will be used instead of default value to serialize query strings in url * @return {{init: init}} */ @@ -82,10 +83,10 @@ export default ({ agent, querySerializer, }: { - agent?: { http: Agent; https: Agent }; + agent?: { http: Dispatcher; https: Dispatcher }; querySerializer?: QuerySerializer; } = {}): Plugin => { - let customAgent: (url: string) => undefined | Agent = () => undefined; + let customAgent: (url: string) => undefined | Dispatcher = () => undefined; if (!isBrowser && agent) { customAgent = (url) => { @@ -93,8 +94,8 @@ export default ({ const parsedUrl = new URL(url); if (parsedUrl.protocol === 'http:') { - if (!isProvidedAgentUndiciInstance(agent.http)) { - console.warn(getWarnMessage('http')); + if (isProvidedAgentBelongsNode(agent.http)) { + showWarn('http'); return undefined; } @@ -102,8 +103,8 @@ export default ({ } } catch (_err) {} - if (!isProvidedAgentUndiciInstance(agent.https)) { - console.warn(getWarnMessage('https')); + if (isProvidedAgentBelongsNode(agent.https)) { + showWarn('https'); return undefined; }