Exemplo de um reverse proxy com Fastify ou Next.JS

As vezes rodar seu reverse proxy local, pode ajudar

TL;DR

Github repo: https://github.com/vNNi/nodejs-reverse-proxy

Contexto

No último mês, trabalhei na performance do site do Magalu, com o objetivo de aumentar o score do Core Web Vitals.

E um dos pontos principais que temos hoje, é a inclusão de muitos scripts de terceiros (marketing e analytics). Esses scripts acabam onerando a performance do site. Então, o que fizemos, foi utilizar a lib Partytown no carregamento desses códigos externos fora da Main Thread. Futuramente irei escrever sobre esse processo e os ganhos.

Mas porque então um Reverse Proxy?

Uma das features da lib Partytown, é buscar os scripts de terceiros utilizando uma request HTTP do browser (fetch), porém, em muitos casos, em um erro de CORS e o recomendado é a utilização de um reverse proxy!.

Então, para testes locais (e validar se o uso da lib de Partytown), criei um servidor local que local como Reverse Proxy. E por não ter encontrado nenhuma referência funcional e simples, resolvi escrever aqui.

Exemplos

Fastify

ref: https://github.com/vNNi/nodejs-reverse-proxy/blob/main/fastify-reverse-proxy.mjs

.js

import { Agent } from "undici";
 
import Fastify from "fastify";
import cors from "@fastify/cors";
 
import { encodeContent } from "./encode-content.mjs";
 
const fastify = Fastify({
  logger: true,
});
await fastify.register(cors, {});
 
fastify.get("/proxy", async function handler(request, reply) {
  const targetUrl = decodeURIComponent(request.query?.target || "");
  let parsedUrl;
 
  if (!targetUrl) {
    return reply.code(400).send({ error: "should has target url for proxy" });
  }
 
  try {
    parsedUrl = new URL(targetUrl);
  } catch (error) {
    parsedUrl = null;
  }
 
  if (!parsedUrl) {
    return reply.status(204).send();
  }
 
  try {
    const urlFinal = parsedUrl.toString();
    const agent = new Agent({
      connect: {
        keepAlive: true,
        rejectUnauthorized: false,
      },
    });
 
    const currentHeaders = new Headers({ ...Object(request.raw.headers) });
 
    const response = await fetch(urlFinal, {
      dispatcher: agent,
      method: request.method,
      mode: "no-cors",
      headers: currentHeaders,
    });
    const data = await response.text();
 
    if (response.headers.get("content-encoding")) {
      /**
       * we should encode to return...
       * ref: https://nodejs.org/api/zlib.html
       */
      // encodeContent(response, data)
    }
    // this doesn't work...
    // on: http://localhost:3000/proxy?target=https%3A%2F%2Fanalytics.tiktok.com/i18n/pixel/static/main.MTc5M2Y0YjUwMQ.js
    response.headers.forEach((value, key) => {
      /**
       * Here we need (for while) we need to ignore
       * encoding or instead encode de data with same
       * encoding algorithm, otherwise we receive the error: ERR_CONTENT_DECODING_FAILED
       * https://kinsta.com/knowledgebase/err_content_decoding_failed/
       */
      if (["content-encoding"].includes(key)) {
        return;
      }
      reply.header(key, value);
    });
 
    reply.header("Content-Type", response.headers.get("content-type") || "");
 
    return reply.status(response.status).send(data);
  } catch (error) {
    return reply.status(500).send({ error: "true" });
  }
});
 
try {
  await fastify.listen({ port: 3000 });
} catch (err) {
  fastify.log.error(err);
  process.exit(1);
}

Rodando servidor:

.sh

 node fastify-reverse-proxy.mjs

Testando:

.sh

 curl "http://localhost:3000/proxy?target=https://analytics.tiktok.com/i18n/pixel/static/main.MTc5M2Y0YjUwMQ.js"

Next.JS - API feature

Conteúdo do arquivo está aqui:

https://github.com/vNNi/nodejs-reverse-proxy/blob/main/next-js-api-handler.mjs

E a estrutura final do projeto deve ficar:

.sh

|--/src
  |--/api
    |--proxy.ts

O uso é igual ao do com Fastify:

.sh

curl "http://localhost:3000/proxy?target=https://analytics.tiktok.com/i18n/pixel/static/main.MTc5M2Y0YjUwMQ.js"