diff --git a/src/media/__tests__/media.test.ts b/src/media/__tests__/media.test.ts index 8c947d85..dc52a62c 100644 --- a/src/media/__tests__/media.test.ts +++ b/src/media/__tests__/media.test.ts @@ -973,6 +973,59 @@ test('media transfer client applies separate upload/download agents', async () = } }) +test('media transfer client uses fetch when no proxy agent is configured', async () => { + const server = http.createServer((request, response) => { + request.resume() + request.on('end', () => { + response.writeHead(request.method === 'POST' ? 201 : 200, { + 'content-type': 'text/plain' + }) + response.end(request.method === 'POST' ? 'upload-fetch-ok' : 'download-fetch-ok') + }) + }) + await new Promise((resolve, reject) => { + server.once('error', reject) + server.listen(0, '127.0.0.1', () => { + server.off('error', reject) + resolve() + }) + }) + const address = server.address() + if (!address || typeof address === 'string') { + throw new Error('failed to resolve media fetch test server address') + } + + const originalFetch = globalThis.fetch + let fetchCalls = 0 + globalThis.fetch = async (...args) => { + fetchCalls++ + return originalFetch(...args) + } + + try { + const mediaTransfer = new WaMediaTransferClient() + const base = `http://127.0.0.1:${address.port}` + const download = await mediaTransfer.downloadBytes({ url: `${base}/download` }) + const uploadResponse = await mediaTransfer.uploadStream({ + url: `${base}/upload`, + method: 'POST', + contentType: 'application/octet-stream', + body: new Uint8Array([1, 2, 3]) + }) + const upload = await mediaTransfer.readResponseBytes(uploadResponse) + + assert.equal(fetchCalls, 2) + assert.equal(new TextDecoder().decode(download), 'download-fetch-ok') + assert.equal(uploadResponse.status, 201) + assert.equal(new TextDecoder().decode(upload), 'upload-fetch-ok') + } finally { + globalThis.fetch = originalFetch + await new Promise((resolve) => { + server.close(() => resolve()) + }) + } +}) + test('media transfer client routes through optional got when proxy agent is set', async () => { const server = http.createServer((_request, response) => { response.writeHead(200, { 'content-type': 'text/plain' }) diff --git a/src/media/transfer/WaMediaTransferClient.ts b/src/media/transfer/WaMediaTransferClient.ts index a7db8f9d..644df1ec 100644 --- a/src/media/transfer/WaMediaTransferClient.ts +++ b/src/media/transfer/WaMediaTransferClient.ts @@ -1,6 +1,6 @@ import http from 'node:http' import https from 'node:https' -import type { Readable } from 'node:stream' +import { Readable } from 'node:stream' import type { Logger } from '@infra/log/types' import { DEFAULT_MEDIA_HOSTS } from '@media/constants' @@ -311,6 +311,35 @@ export class WaMediaTransferClient { init: TransferRequestInit, agent: WaProxyAgent | undefined ): Promise { + if (!agent && typeof fetch === 'function') { + const fetchInit = { + method: init.method ?? 'GET', + headers: init.headers, + body: init.body, + signal: init.signal ?? undefined + } as Parameters[1] & { duplex?: 'half' } + if (init.body && !(init.body instanceof Uint8Array)) { + fetchInit.duplex = 'half' + } + + const response = await fetch(url, fetchInit) + const headers: Record = {} + response.headers.forEach((value, key) => { + headers[key] = value + }) + const body = response.body ? Readable.fromWeb(response.body) : null + return { + status: response.status, + ok: response.ok, + headers, + body, + // eslint-disable-next-line @typescript-eslint/require-await + cancel: async () => { + body?.destroy() + } + } + } + const parsed = new URL(url) const transport = parsed.protocol === 'https:' ? https : http return new Promise((resolve, reject) => {