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 be4f9a3..ba3670a 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, EnvHttpProxyAgent } 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 MockedAgent(); + const mockedAgent = new Agent(); - 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', @@ -251,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 c98c74c..90bf2ba 100644 --- a/packages/plugin-protocol-http/src/http.ts +++ b/packages/plugin-protocol-http/src/http.ts @@ -3,13 +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 type { Agent } from 'undici/types'; +import type { Dispatcher } from 'undici/types'; declare module '@tinkoff/request-core/lib/types.h' { export interface Request { @@ -49,6 +49,14 @@ declare module '@tinkoff/request-core/lib/types.h' { } const isBrowser = typeof window !== 'undefined'; +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 isProvidedAgentBelongsNode(agent: HttpAgent | HttpsAgent | Dispatcher): boolean { + return agent instanceof HttpAgent || agent instanceof HttpsAgent; +} /** * Makes http/https request. @@ -67,7 +75,7 @@ const isBrowser = typeof window !== 'undefined'; * 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}} */ @@ -75,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) => { @@ -86,10 +94,20 @@ export default ({ const parsedUrl = new URL(url); if (parsedUrl.protocol === 'http:') { + if (isProvidedAgentBelongsNode(agent.http)) { + showWarn('http'); + return undefined; + } + return agent.http; } } catch (_err) {} + if (isProvidedAgentBelongsNode(agent.https)) { + showWarn('https'); + return undefined; + } + return agent.https; }; }