Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Significant Slowdown in Requests When Using ProxyAgent #3403

Open
hamdallah90 opened this issue Jul 15, 2024 · 4 comments
Open

Significant Slowdown in Requests When Using ProxyAgent #3403

hamdallah90 opened this issue Jul 15, 2024 · 4 comments

Comments

@hamdallah90
Copy link

Hello,

I'm experiencing a significant slowdown in request performance when using ProxyAgent with the undici library. The same requests without a proxy are much faster. Below are the details of my setup and the observed behavior.

Steps to Reproduce:

  1. Set up a basic HTTP client using undici:
import {
  Client,
  Dispatcher,
  request,
  ProxyAgent,
  getGlobalDispatcher,
} from "undici";
import { Readable, Writable } from "stream";
import { IncomingHttpHeaders } from "undici/types/header";
import { createGunzip } from "zlib";
import { promisify } from "util";
import { Buffer } from "buffer";

const pipeline = promisify(require("stream").pipeline);

export class HttpClient {
  private baseURL: string;
  private defaultHeaders: Record<string, string> = {};
  private proxy: string | undefined;
  private timeout: number = 30000;
  private dispatcher: Dispatcher;

  constructor(baseURL: string, proxyneeded: boolean = true, timeout?: number) {
    this.baseURL = baseURL;

    if (timeout) {
      this.timeout = timeout;
    }

    if (proxyneeded) {
      this.proxy = "http://127.0.0.1:3128";
      this.dispatcher = new ProxyAgent(this.proxy);
    } else {
      this.dispatcher = getGlobalDispatcher();
    }
  }

  public get<T = any>(
    url: string,
    config?: RequestInit & { params?: Record<string, any> }
  ): Promise<T> {
    return this.request<T>({
      ...config,
      method: "GET",
      path: this.buildUrl(url, config?.params),
    });
  }

  public post<T = any>(
    url: string,
    data?: any,
    config?: RequestInit
  ): Promise<T> {
    return this.request<T>({
      ...config,
      method: "POST",
      path: url,
      body: data,
    });
  }

  public put<T = any>(
    url: string,
    data?: any,
    config?: RequestInit
  ): Promise<T> {
    return this.request<T>({ ...config, method: "PUT", path: url, body: data });
  }

  public delete<T = any>(
    url: string,
    config?: RequestInit & { params?: Record<string, any> }
  ): Promise<T> {
    return this.request<T>({
      ...config,
      method: "DELETE",
      path: this.buildUrl(url, config?.params),
    });
  }

  public request<T = any>(
    config: RequestInit & { path: string }
  ): Promise<T> {
    const { method, path, body, headers } = config;

    if (!method) {
      return Promise.reject(new Error("HTTP method is required"));
    }

    const url = !path.startsWith("/") ? `/${path}` : path;

    return request(this.baseURL + url, {
      method: method as Dispatcher.HttpMethod,
      body: body ? JSON.stringify(body) : undefined,
      headers: {
        ...this.defaultHeaders,
        ...headers,
      } as
        | IncomingHttpHeaders
        | string[]
        | Iterable<[string, string | string[] | undefined]>
        | null,
      headersTimeout: this.timeout,
      dispatcher: this.dispatcher,
    } as any)
      .then(({ statusCode, headers, trailers, body }: any) => {
        return this.getResponseBody(headers, body);
      })
      .then(responseContent => {
        return { data: responseContent } as T;
      });
  }

  public setHeader(name: string, value: string): void {
    this.defaultHeaders[name] = value;
  }

  public removeHeader(name: string): void {
    delete this.defaultHeaders[name];
  }

  private buildUrl(url: string, params?: Record<string, any>): string {
    if (!params) {
      return url;
    }

    const urlObj = new URL(url, this.baseURL);
    Object.keys(params).forEach((key) =>
      urlObj.searchParams.append(key, params[key])
    );
    return urlObj.toString();
  }

  private getResponseBody(
    headers: IncomingHttpHeaders,
    response: Dispatcher.BodyMixin
  ): Promise<any> {
    const buffers: Uint8Array[] = [];
    return new Promise((resolve, reject) => {
      (async () => {
        try {
          for await (const chunk of response as any) {
            buffers.push(chunk);
          }
          const buffer = Buffer.concat(buffers);

          if (headers["content-encoding"] === "gzip") {
            this.decompressGzip(buffer).then(resolve).catch(reject);
          } else {
            resolve(this.parseResponse(buffer));
          }
        } catch (err) {
          reject(err);
        }
      })();
    });
  }

  private decompressGzip(buffer: Buffer): Promise<any> {
    const gunzip = createGunzip();
    const decompressedBuffers: Buffer[] = [];
    return new Promise((resolve, reject) => {
      pipeline(
        Readable.from([buffer]),
        gunzip,
        new Writable({
          write(chunk, encoding, callback) {
            decompressedBuffers.push(chunk);
            callback();
          },
        })
      )
        .then(() => {
          const decompressed = Buffer.concat(decompressedBuffers);
          resolve(this.parseResponse(decompressed));
        })
        .catch(reject);
    });
  }

  private parseResponse(buffer: Buffer): any {
    const responseText = buffer.toString();
    try {
      return JSON.parse(responseText);
    } catch {
      return responseText;
    }
  }
}
  1. Compare the response time when using ProxyAgent versus without using any proxy.

Thank you for your attention to this issue. Any insights or fixes would be greatly appreciated.

Best regards,

@metcoder95
Copy link
Member

👋
Can you provide an
Minimum Reproducible Example with plain JS to support you better?

@hamdallah90
Copy link
Author

node httpClientExample.mjs

Making 100 requests without Proxy...
Total time without Proxy: 10058.17 ms
Average time without Proxy: 100.57 ms

Making 100 requests with Proxy...
Total time with Proxy: 45844.97 ms
Average time with Proxy: 458.43 ms

this is the code

import { request, ProxyAgent, getGlobalDispatcher } from "undici";
import { createGunzip } from "zlib";
import { pipeline, Readable, Writable } from "stream";
import { promisify } from "util";
import { Buffer } from "buffer";

const pipelinePromise = promisify(pipeline);


const baseURL = "https://jsonplaceholder.typicode.com";
const proxyURL = "http://127.0.0.1:3128"; // Change this to your actual proxy URL
const timeout = 30000;
const numberOfRequests = 100;

async function fetchWithUndici(url, useProxy = false) {
  const dispatcher = useProxy ? new ProxyAgent(proxyURL) : getGlobalDispatcher();

  const { statusCode, headers, body } = await request(`${baseURL}${url}`, {
    method: 'GET',
    dispatcher,
    headersTimeout: timeout
  });

  const responseBody = await getResponseBody(headers, body);
  return { statusCode, headers, responseBody };
}

async function getResponseBody(headers, body) {
  const buffers = [];
  for await (const chunk of body) {
    buffers.push(chunk);
  }
  const buffer = Buffer.concat(buffers);

  if (headers['content-encoding'] === 'gzip') {
    return decompressGzip(buffer);
  }
  return parseResponse(buffer);
}

function decompressGzip(buffer) {
  const gunzip = createGunzip();
  const decompressedBuffers = [];

  return new Promise((resolve, reject) => {
    pipelinePromise(
      Readable.from([buffer]),
      gunzip,
      new Writable({
        write(chunk, encoding, callback) {
          decompressedBuffers.push(chunk);
          callback();
        }
      })
    )
      .then(() => {
        const decompressed = Buffer.concat(decompressedBuffers);
        resolve(parseResponse(decompressed));
      })
      .catch(reject);
  });
}

function parseResponse(buffer) {
  const responseText = buffer.toString();
  try {
    return JSON.parse(responseText);
  } catch {
    return responseText;
  }
}

async function measureRequests(useProxy) {
  const times = [];
  const startTotal = process.hrtime();

  for (let i = 0; i < numberOfRequests; i++) {
    const start = process.hrtime();
    await fetchWithUndici("/posts/1", useProxy);
    const end = process.hrtime(start);
    const timeInMs = end[0] * 1000 + end[1] / 1e6;
    times.push(timeInMs);
  }

  const endTotal = process.hrtime(startTotal);
  const totalTime = endTotal[0] * 1000 + endTotal[1] / 1e6;
  const averageTime = times.reduce((a, b) => a + b, 0) / times.length;

  return { totalTime, averageTime };
}

(async () => {
  console.log(`Making ${numberOfRequests} requests without Proxy...`);
  const { totalTime: totalTimeWithoutProxy, averageTime: averageTimeWithoutProxy } = await measureRequests(false);
  console.log(`Total time without Proxy: ${totalTimeWithoutProxy.toFixed(2)} ms`);
  console.log(`Average time without Proxy: ${averageTimeWithoutProxy.toFixed(2)} ms`);

  console.log(`Making ${numberOfRequests} requests with Proxy...`);
  const { totalTime: totalTimeWithProxy, averageTime: averageTimeWithProxy } = await measureRequests(true);
  console.log(`Total time with Proxy: ${totalTimeWithProxy.toFixed(2)} ms`);
  console.log(`Average time with Proxy: ${averageTimeWithProxy.toFixed(2)} ms`);
})();

@hamdallah90
Copy link
Author

this is when using axios

node httpClientExample.mjs

Making 100 requests without Proxy...
Total time without Proxy: 10229.93 ms
Average time without Proxy: 102.29 ms

Making 100 requests with Proxy...
Total time with Proxy: 20142.50 ms
Average time with Proxy: 201.41 ms


This is when using axios with Promise.all

node httpClientExample.mjs
Making 100 requests without Proxy...
Total time without Proxy: 535.43 ms
Average time without Proxy: 407.45 ms
Making 100 requests with Proxy...
Total time with Proxy: 628.32 ms
Average time with Proxy: 428.96 ms


This is when using undici with Promise.all

❯ node httpClientExample.mjs
Making 100 requests without Proxy...
Total time without Proxy: 499.42 ms
Average time without Proxy: 373.25 ms
Making 100 requests with Proxy...
Total time with Proxy: 16655.35 ms
Average time with Proxy: 7610.46 ms

@mcollina
Copy link
Member

Looks like there is a bug somewhere, good spot!

What would be good is to have a complete way to reproduce your problem. I recommend you to create a repository and include everything, so we can reproduce this benchmark locally.

(yes, it should include a target server and the proxy).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants