http-proxy-middleware#Options TypeScript Examples

The following examples show how to use http-proxy-middleware#Options. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: http-proxy-middleware.ts    From the-fake-backend with ISC License 5 votes vote down vote up
createProxyMiddleware = (proxy: Options) =>
  jest.fn((_req, _res, next) => next(proxy.target))
Example #2
Source File: router.test.ts    From backstage with Apache License 2.0 4 votes vote down vote up
describe('buildMiddleware', () => {
  const logger = getVoidLogger();

  beforeEach(() => {
    mockCreateProxyMiddleware.mockClear();
  });

  it('accepts strings prefixed by /', async () => {
    buildMiddleware('/proxy', logger, '/test', 'http://mocked');

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
      Options,
    ];
    expect(filter('', { method: 'GET', headers: {} })).toBe(true);
    expect(filter('', { method: 'POST', headers: {} })).toBe(true);
    expect(filter('', { method: 'PUT', headers: {} })).toBe(true);
    expect(filter('', { method: 'PATCH', headers: {} })).toBe(true);
    expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);

    expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/?': '/' });
    expect(fullConfig.changeOrigin).toBe(true);
    expect(fullConfig.logProvider!(logger)).toBe(logger);
  });

  it('accepts routes not prefixed with / when path is not suffixed with /', async () => {
    buildMiddleware('/proxy', logger, 'test', 'http://mocked');

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
      Options,
    ];
    expect(filter('', { method: 'GET', headers: {} })).toBe(true);
    expect(filter('', { method: 'POST', headers: {} })).toBe(true);
    expect(filter('', { method: 'PUT', headers: {} })).toBe(true);
    expect(filter('', { method: 'PATCH', headers: {} })).toBe(true);
    expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);

    expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/?': '/' });
    expect(fullConfig.changeOrigin).toBe(true);
    expect(fullConfig.logProvider!(logger)).toBe(logger);
  });

  it('accepts routes prefixed with / when path is suffixed with /', async () => {
    buildMiddleware('/proxy/', logger, '/test', 'http://mocked');

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
      Options,
    ];
    expect(filter('', { method: 'GET', headers: {} })).toBe(true);
    expect(filter('', { method: 'POST', headers: {} })).toBe(true);
    expect(filter('', { method: 'PUT', headers: {} })).toBe(true);
    expect(filter('', { method: 'PATCH', headers: {} })).toBe(true);
    expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);

    expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/?': '/' });
    expect(fullConfig.changeOrigin).toBe(true);
    expect(fullConfig.logProvider!(logger)).toBe(logger);
  });

  it('limits allowedMethods', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
      allowedMethods: ['GET', 'DELETE'],
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter, fullConfig] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
      Options,
    ];
    expect(filter('', { method: 'GET', headers: {} })).toBe(true);
    expect(filter('', { method: 'POST', headers: {} })).toBe(false);
    expect(filter('', { method: 'PUT', headers: {} })).toBe(false);
    expect(filter('', { method: 'PATCH', headers: {} })).toBe(false);
    expect(filter('', { method: 'DELETE', headers: {} })).toBe(true);

    expect(fullConfig.pathRewrite).toEqual({ '^/proxy/test/?': '/' });
    expect(fullConfig.changeOrigin).toBe(true);
    expect(fullConfig.logProvider!(logger)).toBe(logger);
  });

  it('permits default headers', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
    ];

    const testHeaders = {
      'cache-control': 'mocked',
      'content-language': 'mocked',
      'content-length': 'mocked',
      'content-type': 'mocked',
      expires: 'mocked',
      'last-modified': 'mocked',
      pragma: 'mocked',
      host: 'mocked',
      accept: 'mocked',
      'accept-language': 'mocked',
      'user-agent': 'mocked',
      cookie: 'mocked',
    } as Partial<http.IncomingHttpHeaders>;
    const expectedHeaders = {
      ...testHeaders,
    } as Partial<http.IncomingHttpHeaders>;
    delete expectedHeaders.cookie;

    expect(testHeaders).toBeDefined();
    expect(expectedHeaders).toBeDefined();
    expect(testHeaders).not.toEqual(expectedHeaders);
    expect(filter).toBeDefined();

    filter!('', { method: 'GET', headers: testHeaders });

    expect(testHeaders).toEqual(expectedHeaders);
  });

  it('permits default and configured headers', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
      headers: {
        Authorization: 'my-token',
      },
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
    ];

    const testHeaders = {
      authorization: 'mocked',
      cookie: 'mocked',
    } as Partial<http.IncomingHttpHeaders>;
    const expectedHeaders = {
      ...testHeaders,
    } as Partial<http.IncomingHttpHeaders>;
    delete expectedHeaders.cookie;

    expect(testHeaders).toBeDefined();
    expect(expectedHeaders).toBeDefined();
    expect(testHeaders).not.toEqual(expectedHeaders);
    expect(filter).toBeDefined();

    filter!('', { method: 'GET', headers: testHeaders });

    expect(testHeaders).toEqual(expectedHeaders);
  });

  it('permits configured headers', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
      allowedHeaders: ['authorization', 'cookie'],
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const [filter] = mockCreateProxyMiddleware.mock.calls[0] as [
      (pathname: string, req: Partial<http.IncomingMessage>) => boolean,
    ];

    const testHeaders = {
      authorization: 'mocked',
      cookie: 'mocked',
      'x-auth-request-user': 'mocked',
    } as Partial<http.IncomingHttpHeaders>;
    const expectedHeaders = {
      ...testHeaders,
    } as Partial<http.IncomingHttpHeaders>;
    delete expectedHeaders['x-auth-request-user'];

    expect(testHeaders).toBeDefined();
    expect(expectedHeaders).toBeDefined();
    expect(testHeaders).not.toEqual(expectedHeaders);
    expect(filter).toBeDefined();

    filter!('', { method: 'GET', headers: testHeaders });

    expect(testHeaders).toEqual(expectedHeaders);
  });

  it('responds default headers', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const config = mockCreateProxyMiddleware.mock.calls[0][1] as Options;

    const testClientResponse = {
      headers: {
        'cache-control': 'value',
        'content-language': 'value',
        'content-length': 'value',
        'content-type': 'value',
        expires: 'value',
        'last-modified': 'value',
        pragma: 'value',
        'set-cookie': ['value'],
      },
    } as Partial<http.IncomingMessage>;

    expect(config).toBeDefined();
    expect(config.onProxyRes).toBeDefined();

    config.onProxyRes!(
      testClientResponse as http.IncomingMessage,
      {} as Request,
      {} as Response,
    );

    expect(Object.keys(testClientResponse.headers!)).toEqual([
      'cache-control',
      'content-language',
      'content-length',
      'content-type',
      'expires',
      'last-modified',
      'pragma',
    ]);
  });

  it('responds configured headers', async () => {
    buildMiddleware('/proxy', logger, '/test', {
      target: 'http://mocked',
      allowedHeaders: ['set-cookie'],
    });

    expect(createProxyMiddleware).toHaveBeenCalledTimes(1);

    const config = mockCreateProxyMiddleware.mock.calls[0][1] as Options;

    const testClientResponse = {
      headers: {
        'set-cookie': [],
        'x-auth-request-user': 'asd',
      },
    } as Partial<http.IncomingMessage>;

    expect(config).toBeDefined();
    expect(config.onProxyRes).toBeDefined();

    config.onProxyRes!(
      testClientResponse as http.IncomingMessage,
      {} as Request,
      {} as Response,
    );

    expect(Object.keys(testClientResponse.headers!)).toEqual(['set-cookie']);
  });

  it('rejects malformed target URLs', async () => {
    expect(() =>
      buildMiddleware('/proxy', logger, '/test', 'backstage.io'),
    ).toThrowError(/Proxy target is not a valid URL/);
    expect(() =>
      buildMiddleware('/proxy', logger, '/test', { target: 'backstage.io' }),
    ).toThrowError(/Proxy target is not a valid URL/);
  });
});
Example #3
Source File: core-node-rpc-proxy.ts    From stacks-blockchain-api with GNU General Public License v3.0 4 votes vote down vote up
export function createCoreNodeRpcProxyRouter(db: DataStore): express.Router {
  const router = express.Router();
  router.use(cors());

  const stacksNodeRpcEndpoint = GetStacksNodeProxyEndpoint();

  logger.info(`/v2/* proxying to: ${stacksNodeRpcEndpoint}`);

  // Note: while keep-alive may result in some performance improvements with the stacks-node http server,
  // it can also cause request distribution issues when proxying to a pool of stacks-nodes. See:
  // https://github.com/hirosystems/stacks-blockchain-api/issues/756
  const httpAgent = new Agent({
    // keepAlive: true,
    keepAlive: false, // `false` is the default -- set it explicitly for readability anyway.
    // keepAliveMsecs: 60000,
    maxSockets: 200,
    maxTotalSockets: 400,
  });

  const PROXY_CACHE_CONTROL_FILE_ENV_VAR = 'STACKS_API_PROXY_CACHE_CONTROL_FILE';
  let proxyCacheControlFile = '.proxy-cache-control.json';
  if (process.env[PROXY_CACHE_CONTROL_FILE_ENV_VAR]) {
    proxyCacheControlFile = process.env[PROXY_CACHE_CONTROL_FILE_ENV_VAR] as string;
    logger.info(`Using ${proxyCacheControlFile}`);
  }
  const cacheControlFileWatcher = chokidar.watch(proxyCacheControlFile, {
    persistent: false,
    useFsEvents: false,
    ignoreInitial: true,
  });
  let pathCacheOptions = new Map<RegExp, string | null>();

  const updatePathCacheOptions = () => {
    try {
      const configContent: { paths: Record<string, string> } = jsoncParser.parse(
        fs.readFileSync(proxyCacheControlFile, 'utf8')
      );
      pathCacheOptions = new Map(
        Object.entries(configContent.paths).map(([k, v]) => [RegExp(k), v])
      );
    } catch (error) {
      pathCacheOptions.clear();
      logger.error(`Error reading changes from ${proxyCacheControlFile}`, error);
    }
  };
  updatePathCacheOptions();
  cacheControlFileWatcher.on('all', (eventName, path, stats) => {
    updatePathCacheOptions();
  });

  const getCacheControlHeader = (statusCode: number, url: string): string | null => {
    if (statusCode < 200 || statusCode > 299) {
      return null;
    }
    for (const [regexp, cacheControl] of pathCacheOptions.entries()) {
      if (cacheControl && regexp.test(url)) {
        return cacheControl;
      }
    }
    return null;
  };

  /**
   * Check for any extra endpoints that have been configured for performing a "multicast" for a tx submission.
   */
  async function getExtraTxPostEndpoints(): Promise<string[] | false> {
    const STACKS_API_EXTRA_TX_ENDPOINTS_FILE_ENV_VAR = 'STACKS_API_EXTRA_TX_ENDPOINTS_FILE';
    const extraEndpointsEnvVar = process.env[STACKS_API_EXTRA_TX_ENDPOINTS_FILE_ENV_VAR];
    if (!extraEndpointsEnvVar) {
      return false;
    }
    const filePath = path.resolve(REPO_DIR, extraEndpointsEnvVar);
    let fileContents: string;
    try {
      fileContents = await fs.promises.readFile(filePath, { encoding: 'utf8' });
    } catch (error) {
      logError(`Error reading ${STACKS_API_EXTRA_TX_ENDPOINTS_FILE_ENV_VAR}: ${error}`, error);
      return false;
    }
    const endpoints = fileContents
      .split(/\r?\n/)
      .map(r => r.trim())
      .filter(r => !r.startsWith('#') && r.length !== 0);
    if (endpoints.length === 0) {
      return false;
    }
    return endpoints;
  }

  /**
   * Reads an http request stream into a Buffer.
   */
  async function readRequestBody(req: express.Request, maxSizeBytes = Infinity): Promise<Buffer> {
    return new Promise((resolve, reject) => {
      let resultBuffer: Buffer = Buffer.alloc(0);
      req.on('data', chunk => {
        if (!Buffer.isBuffer(chunk)) {
          reject(
            new Error(
              `Expected request body chunks to be Buffer, received ${chunk.constructor.name}`
            )
          );
          req.destroy();
          return;
        }
        resultBuffer = resultBuffer.length === 0 ? chunk : Buffer.concat([resultBuffer, chunk]);
        if (resultBuffer.byteLength >= maxSizeBytes) {
          reject(new Error(`Request body exceeded max byte size`));
          req.destroy();
          return;
        }
      });
      req.on('end', () => {
        if (!req.complete) {
          return reject(
            new Error('The connection was terminated while the message was still being sent')
          );
        }
        resolve(resultBuffer);
      });
      req.on('error', error => reject(error));
    });
  }

  /**
   * Logs a transaction broadcast event alongside the current block height.
   */
  async function logTxBroadcast(response: string): Promise<void> {
    try {
      const blockHeightQuery = await db.getCurrentBlockHeight();
      if (!blockHeightQuery.found) {
        return;
      }
      const blockHeight = blockHeightQuery.result;
      // Strip wrapping double quotes (if any)
      const txId = response.replace(/^"(.*)"$/, '$1');
      logger.info('Transaction broadcasted', {
        txid: `0x${txId}`,
        first_broadcast_at_stacks_height: blockHeight,
      });
    } catch (error) {
      logError(`Error logging tx broadcast: ${error}`, error);
    }
  }

  router.post(
    '/transactions',
    asyncHandler(async (req, res, next) => {
      const extraEndpoints = await getExtraTxPostEndpoints();
      if (!extraEndpoints) {
        next();
        return;
      }
      const endpoints = [
        // The primary proxy endpoint (the http response from this one will be returned to the client)
        `http://${stacksNodeRpcEndpoint}/v2/transactions`,
      ];
      endpoints.push(...extraEndpoints);
      logger.info(`Overriding POST /v2/transactions to multicast to ${endpoints.join(',')}}`);
      const maxBodySize = 10_000_000; // 10 MB max POST body size
      const reqBody = await readRequestBody(req, maxBodySize);
      const reqHeaders: string[][] = [];
      for (let i = 0; i < req.rawHeaders.length; i += 2) {
        reqHeaders.push([req.rawHeaders[i], req.rawHeaders[i + 1]]);
      }
      const postFn = async (endpoint: string) => {
        const reqOpts: RequestInit = {
          method: 'POST',
          agent: httpAgent,
          body: reqBody,
          headers: reqHeaders,
        };
        const proxyResult = await fetch(endpoint, reqOpts);
        return proxyResult;
      };

      // Here's were we "multicast" the `/v2/transaction` POST, by concurrently sending the http request to all configured endpoints.
      const results = await Promise.allSettled(endpoints.map(endpoint => postFn(endpoint)));

      // Only the first (non-extra) endpoint http response is proxied back through to the client, so ensure any errors from requests
      // to the extra endpoints are logged.
      results.slice(1).forEach(p => {
        if (p.status === 'rejected') {
          logError(`Error during POST /v2/transaction to extra endpoint: ${p.reason}`, p.reason);
        } else {
          if (!p.value.ok) {
            logError(
              `Response ${p.value.status} during POST /v2/transaction to extra endpoint ${p.value.url}`
            );
          }
        }
      });

      // Proxy the result of the (non-extra) http response back to the client.
      const mainResult = results[0];
      if (mainResult.status === 'rejected') {
        logError(
          `Error in primary POST /v2/transaction proxy: ${mainResult.reason}`,
          mainResult.reason
        );
        res.status(500).json({ error: mainResult.reason });
      } else {
        const proxyResp = mainResult.value;
        res.status(proxyResp.status);
        proxyResp.headers.forEach((value, name) => {
          res.setHeader(name, value);
        });
        if (proxyResp.status === 200) {
          // Log the transaction id broadcast, but clone the `Response` first before parsing its body
          // so we don't mess up the original response's `ReadableStream` pointers.
          const parsedTxId: string = await proxyResp.clone().text();
          await logTxBroadcast(parsedTxId);
        }
        await pipelineAsync(proxyResp.body, res);
      }
    })
  );

  const proxyOptions: Options = {
    agent: httpAgent,
    target: `http://${stacksNodeRpcEndpoint}`,
    changeOrigin: true,
    selfHandleResponse: true,
    onProxyRes: responseInterceptor(async (responseBuffer, proxyRes, req, res) => {
      if (req.url !== undefined) {
        const header = getCacheControlHeader(res.statusCode, req.url);
        if (header) {
          res.setHeader('Cache-Control', header);
        }
        const url = new URL(req.url, `http://${req.headers.host}`);
        if (url.pathname === '/v2/transactions' && res.statusCode === 200) {
          await logTxBroadcast(responseBuffer.toString());
        }
      }
      return responseBuffer;
    }),
    onError: (error, req, res) => {
      const msg =
        (error as any).code === 'ECONNREFUSED'
          ? 'core node unresponsive'
          : 'cannot connect to core node';
      res
        .writeHead(502, { 'Content-Type': 'application/json' })
        .end(JSON.stringify({ message: msg, error: error }));
    },
  };

  const stacksNodeRpcProxy = createProxyMiddleware(proxyOptions);

  router.use(stacksNodeRpcProxy);

  return router;
}