diff --git a/src/utils/api.mts b/src/utils/api.mts index 7582b54a0..9d81f374b 100644 --- a/src/utils/api.mts +++ b/src/utils/api.mts @@ -19,6 +19,7 @@ * - Falls back to configured apiBaseUrl or default API_V0_URL */ +import { request as httpRequest } from 'node:http' import { Agent as HttpsAgent, request as httpsRequest } from 'node:https' import { ReadableStream } from 'node:stream/web' @@ -84,25 +85,27 @@ export type ApiFetchInit = { method?: string | undefined } -// Internal httpsRequest-based fetch with redirect support. -function _httpsRequestFetch( +// Internal node request-based fetch with redirect support. +function _nodeRequestFetch( url: string, init: ApiFetchInit, agent: HttpsAgent | undefined, redirectCount: number, ): Promise { return new Promise((resolve, reject) => { + const parsedUrl = new URL(url) const headers: Record = { ...init.headers } // Set Content-Length for request bodies to avoid chunked transfer encoding. if (init.body) { headers['content-length'] = String(Buffer.byteLength(init.body)) } - const req = httpsRequest( + const request = parsedUrl.protocol === 'http:' ? httpRequest : httpsRequest + const req = request( url, { method: init.method || 'GET', headers, - agent, + agent: parsedUrl.protocol === 'https:' ? agent : undefined, }, res => { const { statusCode } = res @@ -141,7 +144,7 @@ function _httpsRequestFetch( // 307 and 308 preserve the original method and body. const preserveMethod = statusCode === 307 || statusCode === 308 resolve( - _httpsRequestFetch( + _nodeRequestFetch( redirectUrl, preserveMethod ? { ...init, headers: redirectHeaders } @@ -204,7 +207,7 @@ export async function apiFetch( url: string, init: ApiFetchInit = {}, ): Promise { - return await _httpsRequestFetch(url, init, getHttpsAgent(), 0) + return await _nodeRequestFetch(url, init, getHttpsAgent(), 0) } export type CommandRequirements = { diff --git a/src/utils/api.test.mts b/src/utils/api.test.mts index 222960940..e046616f6 100644 --- a/src/utils/api.test.mts +++ b/src/utils/api.test.mts @@ -45,9 +45,13 @@ type RequestCallback = ( }, ) => void const mockHttpsRequest = vi.hoisted(() => vi.fn()) +const mockHttpRequest = vi.hoisted(() => vi.fn()) const MockHttpsAgent = vi.hoisted(() => vi.fn().mockImplementation(opts => ({ ...opts, _isHttpsAgent: true })), ) +vi.mock('node:http', () => ({ + request: mockHttpRequest, +})) vi.mock('node:https', () => ({ Agent: MockHttpsAgent, request: mockHttpsRequest, @@ -156,6 +160,48 @@ describe('apiFetch with extra CA certificates', () => { expect(result.ok).toBe(true) }) + it('should use http.request for plain HTTP API URLs', async () => { + const mockReq = { + end: vi.fn(), + on: vi.fn(), + write: vi.fn(), + } + + mockHttpRequest.mockImplementation( + (_url: string, _opts: unknown, callback: RequestCallback) => { + setTimeout(() => { + const mockRes = { + headers: { 'content-type': 'text/plain' }, + on: vi.fn(), + statusCode: 200, + statusMessage: 'OK', + } + const handlers: Record = {} + mockRes.on.mockImplementation((event: string, handler: Function) => { + handlers[event] = handler + return mockRes + }) + callback(mockRes) + handlers['data']?.(Buffer.from('local response')) + handlers['end']?.() + }, 0) + return mockReq + }, + ) + + const { apiFetch } = await import('./api.mts') + const response = await apiFetch('http://localhost:3000/v0/report') + + expect(response.status).toBe(200) + expect(await response.text()).toBe('local response') + expect(mockHttpRequest).toHaveBeenCalledWith( + 'http://localhost:3000/v0/report', + expect.objectContaining({ agent: undefined, method: 'GET' }), + expect.any(Function), + ) + expect(mockHttpsRequest).not.toHaveBeenCalled() + }) + it('should use https.request when extra CA certs are available', async () => { const caCerts = ['ROOT_CERT', 'EXTRA_CERT'] mockGetExtraCaCerts.mockReturnValue(caCerts)