2
0
mirror of https://github.com/tenrok/axios.git synced 2026-05-15 11:59:42 +03:00

feat(adapter): add fetch adapter; (#6371)

This commit is contained in:
Dmitriy Mozgovoy
2024-04-28 22:33:49 +03:00
committed by GitHub
parent 751133eb9e
commit a3ff99b59d
21 changed files with 1015 additions and 127 deletions
+117
View File
@@ -0,0 +1,117 @@
import http from "http";
import stream from "stream";
import getStream from "get-stream";
import {Throttle} from "stream-throttle";
import formidable from "formidable";
export const LOCAL_SERVER_URL = 'http://localhost:4444';
export const SERVER_HANDLER_STREAM_ECHO = (req, res) => req.pipe(res);
export const setTimeoutAsync = (ms) => new Promise(resolve=> setTimeout(resolve, ms));
export const startHTTPServer = (handlerOrOptions, options) => {
const {handler, useBuffering = false, rate = undefined, port = 4444, keepAlive = 1000} =
Object.assign(typeof handlerOrOptions === 'function' ? {
handler: handlerOrOptions
} : handlerOrOptions || {}, options);
return new Promise((resolve, reject) => {
const server = http.createServer(handler || async function (req, res) {
try {
req.headers['content-length'] && res.setHeader('content-length', req.headers['content-length']);
let dataStream = req;
if (useBuffering) {
dataStream = stream.Readable.from(await getStream(req));
}
let streams = [dataStream];
if (rate) {
streams.push(new Throttle({rate}))
}
streams.push(res);
stream.pipeline(streams, (err) => {
err && console.log('Server warning: ' + err.message)
});
} catch (err){
console.warn('HTTP server error:', err);
}
}).listen(port, function (err) {
err ? reject(err) : resolve(this);
});
server.keepAliveTimeout = keepAlive;
});
}
export const stopHTTPServer = async (server, timeout = 10000) => {
if (server) {
if (typeof server.closeAllConnections === 'function') {
server.closeAllConnections();
}
await Promise.race([new Promise(resolve => server.close(resolve)), setTimeoutAsync(timeout)]);
}
}
export const handleFormData = (req) => {
return new Promise((resolve, reject) => {
const form = new formidable.IncomingForm();
form.parse(req, (err, fields, files) => {
if (err) {
return reject(err);
}
resolve({fields, files});
});
});
}
export const nodeVersion = process.versions.node.split('.').map(v => parseInt(v, 10));
export const generateReadable = (length = 1024 * 1024, chunkSize = 10 * 1024, sleep = 50) => {
return stream.Readable.from(async function* (){
let dataLength = 0;
while(dataLength < length) {
const leftBytes = length - dataLength;
const chunk = Buffer.alloc(leftBytes > chunkSize? chunkSize : leftBytes);
dataLength += chunk.length;
yield chunk;
if (sleep) {
await setTimeoutAsync(sleep);
}
}
}());
}
export const makeReadableStream = (chunk = 'chunk', n = 10, timeout = 100) => {
return new ReadableStream({
async pull(controller) {
await setTimeoutAsync(timeout);
n-- ? controller.enqueue(chunk) : controller.close();
}
},
{
highWaterMark: 1
}
)
}
export const makeEchoStream = (echo) => new WritableStream({
write(chunk) {
echo && console.log(`Echo chunk`, chunk);
}
})
+373
View File
@@ -0,0 +1,373 @@
import assert from 'assert';
import {
startHTTPServer,
stopHTTPServer,
LOCAL_SERVER_URL,
setTimeoutAsync,
makeReadableStream,
generateReadable,
makeEchoStream
} from '../../helpers/server.js';
import axios from '../../../index.js';
import stream from "stream";
import {AbortController} from "abortcontroller-polyfill/dist/cjs-ponyfill.js";
import util from "util";
const pipelineAsync = util.promisify(stream.pipeline);
const fetchAxios = axios.create({
baseURL: LOCAL_SERVER_URL,
adapter: 'fetch'
});
let server;
describe('supports fetch with nodejs', function () {
before(function () {
if (typeof fetch !== 'function') {
this.skip();
}
})
afterEach(async function () {
await stopHTTPServer(server);
server = null;
});
describe('responses', async () => {
it(`should support text response type`, async () => {
const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData));
const {data} = await fetchAxios.get('/', {
responseType: 'text'
});
assert.deepStrictEqual(data, originalData);
});
it(`should support arraybuffer response type`, async () => {
const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData));
const {data} = await fetchAxios.get('/', {
responseType: 'arraybuffer'
});
assert.deepStrictEqual(data, Uint8Array.from(await new TextEncoder().encode(originalData)).buffer);
});
it(`should support blob response type`, async () => {
const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData));
const {data} = await fetchAxios.get('/', {
responseType: 'blob'
});
assert.deepStrictEqual(data, new Blob([originalData]));
});
it(`should support stream response type`, async () => {
const originalData = 'my data';
server = await startHTTPServer((req, res) => res.end(originalData));
const {data} = await fetchAxios.get('/', {
responseType: 'stream'
});
assert.ok(data instanceof ReadableStream, 'data is not instanceof ReadableStream');
let response = new Response(data);
assert.deepStrictEqual(await response.text(), originalData);
});
it(`should support formData response type`, async function () {
this.timeout(5000);
const originalData = new FormData();
originalData.append('x', '123');
server = await startHTTPServer(async (req, res) => {
const response = await new Response(originalData);
res.setHeader('Content-Type', response.headers.get('Content-Type'));
res.end(await response.text());
});
const {data} = await fetchAxios.get('/', {
responseType: 'formdata'
});
assert.ok(data instanceof FormData, 'data is not instanceof FormData');
assert.deepStrictEqual(Object.fromEntries(data.entries()), Object.fromEntries(originalData.entries()));
});
it(`should support json response type`, async () => {
const originalData = {x: 'my data'};
server = await startHTTPServer((req, res) => res.end(JSON.stringify(originalData)));
const {data} = await fetchAxios.get('/', {
responseType: 'json'
});
assert.deepStrictEqual(data, originalData);
});
});
describe("progress", () => {
describe('upload', function () {
it('should support upload progress capturing', async function () {
this.timeout(15000);
server = await startHTTPServer({
rate: 100 * 1024
});
let content = '';
const count = 10;
const chunk = "test";
const chunkLength = Buffer.byteLength(chunk);
const contentLength = count * chunkLength;
const readable = stream.Readable.from(async function* () {
let i = count;
while (i-- > 0) {
await setTimeoutAsync(1100);
content += chunk;
yield chunk;
}
}());
const samples = [];
const {data} = await fetchAxios.post('/', readable, {
onUploadProgress: ({loaded, total, progress, bytes, upload}) => {
console.log(`Upload Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)`);
samples.push({
loaded,
total,
progress,
bytes,
upload
});
},
headers: {
'Content-Length': contentLength
},
responseType: 'text'
});
await setTimeoutAsync(500);
assert.strictEqual(data, content);
assert.deepStrictEqual(samples, Array.from(function* () {
for (let i = 1; i <= 10; i++) {
yield ({
loaded: chunkLength * i,
total: contentLength,
progress: (chunkLength * i) / contentLength,
bytes: 4,
upload: true
});
}
}()));
});
it('should not fail with get method', async() => {
server = await startHTTPServer((req, res) => res.end('OK'));
const {data} = await fetchAxios.get('/', {
onUploadProgress() {
}
});
assert.strictEqual(data, 'OK');
});
});
describe('download', function () {
it('should support download progress capturing', async function () {
this.timeout(15000);
server = await startHTTPServer({
rate: 100 * 1024
});
let content = '';
const count = 10;
const chunk = "test";
const chunkLength = Buffer.byteLength(chunk);
const contentLength = count * chunkLength;
const readable = stream.Readable.from(async function* () {
let i = count;
while (i-- > 0) {
await setTimeoutAsync(1100);
content += chunk;
yield chunk;
}
}());
const samples = [];
const {data} = await fetchAxios.post('/', readable, {
onDownloadProgress: ({loaded, total, progress, bytes, download}) => {
console.log(`Download Progress ${loaded} from ${total} bytes (${(progress * 100).toFixed(1)}%)`);
samples.push({
loaded,
total,
progress,
bytes,
download
});
},
headers: {
'Content-Length': contentLength
},
responseType: 'text',
maxRedirects: 0
});
await setTimeoutAsync(500);
assert.strictEqual(data, content);
assert.deepStrictEqual(samples, Array.from(function* () {
for (let i = 1; i <= 10; i++) {
yield ({
loaded: chunkLength * i,
total: contentLength,
progress: (chunkLength * i) / contentLength,
bytes: 4,
download: true
});
}
}()));
});
});
});
it('should support basic auth', async () => {
server = await startHTTPServer((req, res) => res.end(req.headers.authorization));
const user = 'foo';
const headers = {Authorization: 'Bearer 1234'};
const res = await axios.get('http://' + user + '@localhost:4444/', {headers: headers});
const base64 = Buffer.from(user + ':', 'utf8').toString('base64');
assert.equal(res.data, 'Basic ' + base64);
});
it("should support stream.Readable as a payload", async () => {
server = await startHTTPServer();
const {data} = await fetchAxios.post('/', stream.Readable.from('OK'));
assert.strictEqual(data, 'OK');
});
describe('request aborting', function() {
it('should be able to abort the request stream', async function () {
server = await startHTTPServer({
rate: 100000,
useBuffering: true
});
const controller = new AbortController();
setTimeout(() => {
controller.abort();
}, 500);
await assert.rejects(async () => {
await fetchAxios.post('/', makeReadableStream(), {
responseType: 'stream',
signal: controller.signal
});
}, /CanceledError/);
});
it('should be able to abort the response stream', async function () {
server = await startHTTPServer((req, res) => {
pipelineAsync(generateReadable(10000, 10), res);
});
const controller = new AbortController();
setTimeout(() => {
controller.abort(new Error('test'));
}, 800);
const {data} = await fetchAxios.get('/', {
responseType: 'stream',
signal: controller.signal
});
await assert.rejects(async () => {
await data.pipeTo(makeEchoStream());
}, /^(AbortError|CanceledError):/);
});
});
it('should support a timeout', async () => {
server = await startHTTPServer(async(req, res) => {
await setTimeoutAsync(1000);
res.end('OK');
});
const timeout = 500;
const ts = Date.now();
await assert.rejects(async() => {
await fetchAxios('/', {
timeout
})
}, /timeout/);
const passed = Date.now() - ts;
assert.ok(passed >= timeout - 5, `early cancellation detected (${passed} ms)`);
});
it('should combine baseURL and url', async () => {
server = await startHTTPServer();
const res = await fetchAxios('/foo');
assert.equal(res.config.baseURL, LOCAL_SERVER_URL);
assert.equal(res.config.url, '/foo');
});
it('should support params', async() => {
server = await startHTTPServer((req, res) => res.end(req.url));
const {data} = await fetchAxios.get('/?test=1', {
params: {
foo: 1,
bar: 2
}
});
assert.strictEqual(data, '/?test=1&foo=1&bar=2');
});
});