Net-Base Revista

23.05.2026

Proxy reverso com nginx e Delphi: tratamento correto do cabeçalho Forwarded, IP real do cliente e bases de URL robustas

Wenn Delphi-REST-Server hinter nginx laufen, kippen oft Client-IP, HTTPS-Erkennung und absolute URLs. Dieser Source-Schnipsel zeigt ein robustes Forwarded-/X-Forwarded-Handling (inkl. Trust-Proxy-Liste), typische nginx-Settings und Debugging-Hinweise für den Betrieb.

23.05.2026

Um reverse proxy com nginx e Delphi na prática normalmente não é um „nice to have“, mas a separação limpa entre a borda da Internet e a aplicação: terminação TLS (offloading de HTTPS), regras centrais de header/CORS, limites de taxa, logs homogêneos, rollouts Blue/Green ou simplesmente o hosting de vários serviços sob um domínio. O que costuma ser subestimado é: assim que o nginx fica „na frente“, o servidor Delphi passa a ver apenas o IP do proxy, frequentemente apenas „http“ em vez de „https“ e gera links absolutos incorretos (redirects, URLs de callback, OpenAPI-Server-URL). Exatamente esses três pontos geram depois tempo de depuração em produção.

Este trecho de código mostra um padrão robusto de como avaliar corretamente em Delphi os cabeçalhos Forwarded e X-Forwarded-* — incluindo uma lista de proxies confiáveis (importante contra falsificação de cabeçalhos) e uma URL base da requisição consistente. Há também configurações nginx práticas e indicações para casos-limite como WebSockets, uploads grandes e timeouts.

Por que configurações com reverse proxy confundem servidores Delphi

O nginx, atuando como reverse proxy, normalmente comunica-se com o serviço Delphi sem criptografia (HTTP) na rede interna ou em localhost, enquanto o cliente externo chega via HTTPS. Sem cabeçalhos adicionais, o Delphi não sabe sobre:

  • Esquema original (https vs. http) – relevante para redirecionamentos e URLs absolutas.
  • Host original (domínio do cliente, porta) – relevante para configurações multi-tenant, CORS e URLs de callback.
  • IP original do cliente – relevante para auditoria, limites de taxa, verificações de geolocalização e análises de segurança.

O nginx pode transportar essas informações por meio de cabeçalhos. Comuns são X-Forwarded-For, X-Forwarded-Proto e X-Forwarded-Host; padronizado está adicionalmente o cabeçalho RFC Forwarded. Importante: esses cabeçalhos não são automaticamente confiáveis do ponto de vista da aplicação, porque um cliente pode enviá-los por conta própria — só se tornam confiáveis quando provêm de um proxy conhecido.

Configuração nginx: os cabeçalhos de proxy essenciais

Um ponto de partida sólido (HTTP/1.1, Keep-Alive, Upgrade para WebSockets) é este. O snippet é intencionalmente conciso; você complementa, conforme o ambiente, com HSTS, limites de taxa e logs de acesso.

Delphi
# (nginx-Konfiguration, kein Delphi)
server {
  listen 443 ssl http2;
  server_name api.example.com;

  # ssl_certificate ...; ssl_certificate_key ...;

  location / {
    proxy_http_version 1.1;
    proxy_set_header Host              $host;
    proxy_set_header X-Real-IP         $remote_addr;
    proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;

    # optional, aber praktisch für absolute URLs
    proxy_set_header X-Forwarded-Host  $host;
    proxy_set_header X-Forwarded-Port  $server_port;

    # WebSockets / Upgrade
    proxy_set_header Upgrade           $http_upgrade;
    proxy_set_header Connection        $connection_upgrade;

    proxy_pass http://127.0.0.1:8080;

    # Timeouts passend zu Delphi-Backend (lange Reports/Exports)
    proxy_connect_timeout 5s;
    proxy_send_timeout    60s;
    proxy_read_timeout    60s;

    # große Uploads explizit steuern
    client_max_body_size 50m;
  }
}

Objetivo: A aplicação recebe Host, IP do cliente e esquema encaminhados de forma confiável. Condição: $proxy_add_x_forwarded_for anexa o IP do proxy atual a uma eventual cadeia existente; isso é bom para setups com múltiplos proxies, mas torna a avaliação correta no lado de Delphi ainda mais importante. Aviso: Se você não definir o cabeçalho Host no nginx, Delphi possivelmente verá apenas o Upstream-Host (127.0.0.1), o que quebra redirecionamentos e verificações de origem.

Delphi Trecho de código-fonte: Avaliação robusta de Forwarded/X-Forwarded (com lista de proxies confiáveis)

O código a seguir foi deliberadamente mantido framework-neutral: opera contra uma interface mínima (Header + RemoteIP) e pode ser adaptado para WebBroker, RAD Server ou Horse. Pontos principais:

  • Prioridade: RFC Forwarded (se presente) antes de X-Forwarded-*.
  • Trust: interpretar cabeçalhos Forwarded apenas quando o peer direto (RemoteIP) for um proxy conhecido.
  • Parsing: considerar IPv6, aspas, portas e cadeias em X-Forwarded-For.
  • Output: uma URL base que você pode usar para links absolutos, redirecionamentos ou OpenAPI.
Delphi
unit Net-Base.ProxyForwarding;

interface

uses
  System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;

type
  // Interface mínimo de adaptador: implemente-o para WebBroker/Horse/etc.
  IHeaderReader = interface
    ['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
    function GetHeaderValue(const AName: string): string;
    function GetRemoteIP: string; // contraparte TCP direta (normalmente nginx)
  end;

  TForwardedInfo = record
    ClientIP: string;
    Proto: string; // http/https
    Host: string;
    Port: Integer;
    function EffectiveScheme: string;
    function EffectiveHostPort: string;
    function BaseUrl: string; // ex.: https://api.example.com
  end;

  TTrustedProxyList = class
  private
    FSet: TDictionary;
    class function NormalizeIp(const AIP: string): string; static;
  public
    constructor Create;
    destructor Destroy; override;
    procedure Add(const AIP: string);
    function Contains(const AIP: string): Boolean;
  end;

function ResolveForwardedInfo(const Req: IHeaderReader; const TrustedProxies: TTrustedProxyList): TForwardedInfo;

implementation

function StripBrackets(const S: string): string;
begin
  Result := S.Trim;
  if (Result.StartsWith('[')) and (Result.EndsWith(']')) then
    Result := Result.Substring(1, Result.Length - 2);
end;

function RemoveQuotes(const S: string): string;
var
  T: string;
begin
  T := S.Trim;
  if (T.Length >= 2) and (((T[1] = '"') and (T[T.Length] = '"')) or ((T[1] = '''') and (T[T.Length] = ''''))) then
    Result := T.Substring(1, T.Length - 2)
  else
    Result := T;
end;

function FirstCsvToken(const S: string): string;
var
  P: Integer;
begin
  // X-Forwarded-For pode ser "cliente, proxy1, proxy2"
  P := S.IndexOf(',');
  if P >= 0 then
    Result := S.Substring(0, P).Trim
  else
    Result := S.Trim;
end;

procedure SplitHostPort(const HostPort: string; out Host: string; out Port: Integer);
var
  S: string;
  P: Integer;
begin
  Host := '';
  Port := 0;
  S := HostPort.Trim;

  // IPv6 entre []: [2001:db8::1]:443
  if S.StartsWith('[') then
  begin
    P := S.IndexOf(']');
    if P >= 0 then
    begin
      Host := StripBrackets(S.Substring(0, P + 1));
      if (P + 1 < S.Length) and (S[P + 2] = ':') then
        Port := StrToIntDef(S.Substring(P + 2), 0);
      Exit;
    end;
  end;

  // IPv4/Host: host:port (Aviso: não confiável para IPv6 sem [])
  P := S.LastIndexOf(':');
  if (P > 0) and (S.IndexOf(':') = P) then
  begin
    Host := S.Substring(0, P);
    Port := StrToIntDef(S.Substring(P + 1), 0);
  end
  else
    Host := S;
end;

function ParseForwardedHeader(const ForwardedValue: string; out ClientIP, Proto, Host: string): Boolean;
var
  // Exemplo: Forwarded: for=203.0.113.43;proto=https;host=api.example.com
  Parts: TArray<string>;
  I: Integer;
  KV: TArray<string>;
  K, V: string;
  FirstElement: string;
begin
  Result := False;
  ClientIP := '';
  Proto := '';
  Host := '';

  if ForwardedValue.Trim = '' then
    Exit;

  // Vários elementos são separados por vírgula; usamos o primeiro (o mais próximo do cliente)
  FirstElement := FirstCsvToken(ForwardedValue);
  Parts := FirstElement.Split([';']);
  for I := 0 to High(Parts) do
  begin
    KV := Parts[I].Split(['='], 2);
    if Length(KV) <> 2 then
      Continue;
    K := KV[0].Trim.ToLower;
    V := RemoveQuotes(KV[1]);

    if K = 'for' then
    begin
      // for pode ser IP ou "unknown"; IPv6 pode estar entre []
      V := V.Trim;
      if SameText(V, 'unknown') then
        Continue;
      // for=1.2.3.4:5678 pode ocorrer
      if V.Contains(':') and (not V.StartsWith('[')) then
        V := FirstCsvToken(V); // por precaução
      ClientIP := StripBrackets(V.Split([':'])[0]);
    end
    else if K = 'proto' then
      Proto := V.Trim.ToLower
    else if K = 'host' then
      Host := V.Trim;
  end;

  Result := (ClientIP <> '') or (Proto <> '') or (Host <> '');
end;

{ TForwardedInfo }

function TForwardedInfo.EffectiveScheme: string;
begin
  if Proto <> '' then
    Exit(Proto);
  Result := 'http';
end;

function TForwardedInfo.EffectiveHostPort: string;
begin
  if Host = '' then
    Exit('');

  if (Port > 0) and not ((EffectiveScheme = 'https') and (Port = 443)) and not ((EffectiveScheme = 'http') and (Port = 80)) then
    Result := Host + ':' + Port.ToString
  else
    Result := Host;
end;

function TForwardedInfo.BaseUrl: string;
var
  HP: string;
begin
  HP := EffectiveHostPort;
  if HP = '' then
    Exit('');
  Result := EffectiveScheme + '://' + HP;
end;

{ TTrustedProxyList }

constructor TTrustedProxyList.Create;
begin
  inherited Create;
  FSet := TDictionary<string, Boolean>.Create;
end;

destructor TTrustedProxyList.Destroy;
begin
  FSet.Free;
  inherited;
end;

class function TTrustedProxyList.NormalizeIp(const AIP: string): string;
begin
  // Para uma normalização IPv6 completa seria necessário analisar o IP; aqui, deliberadamente pragmático.
  Result := AIP.Trim;
  Result := StripBrackets(Result);
end;

procedure TTrustedProxyList.Add(const AIP: string);
begin
  FSet.AddOrSetValue(NormalizeIp(AIP), True);
end;

function TTrustedProxyList.Contains(const AIP: string): Boolean;
begin
  Result := FSet.ContainsKey(NormalizeIp(AIP));
end;

function ResolveForwardedInfo(const Req: IHeaderReader; const TrustedProxies: TTrustedProxyList): TForwardedInfo;
var
  RemoteIP: string;
  Fwd, XFF, XProto, XHost, XPort: string;
  ClientIP, Proto, Host: string;
  Port: Integer;
begin
  Result := Default(TForwardedInfo);

  RemoteIP := Req.GetRemoteIP;
  Result.ClientIP := RemoteIP; // Fallback: contraparte direta

  // Apenas se a contraparte direta for um proxy conhecido, analisamos os cabeçalhos Forwarded.
  if (TrustedProxies <> nil) and TrustedProxies.Contains(RemoteIP) then
  begin
    Fwd := Req.GetHeaderValue('Forwarded');
    if ParseForwardedHeader(Fwd, ClientIP, Proto, Host) then
    begin
      if ClientIP <> '' then Result.ClientIP := ClientIP;
      if Proto <> '' then Result.Proto := Proto;
      if Host <> '' then
      begin
        SplitHostPort(Host, Result.Host, Result.Port);
      end;
    end;

    // X-Forwarded-* como fallback/complemento
    XFF := Req.GetHeaderValue('X-Forwarded-For');
    if (Result.ClientIP = RemoteIP) and (XFF.Trim <> '') then
      Result.ClientIP := StripBrackets(FirstCsvToken(XFF));

    XProto := Req.GetHeaderValue('X-Forwarded-Proto');
    if (Result.Proto = '') and (XProto.Trim <> '') then
      Result.Proto := FirstCsvToken(XProto).ToLower;

    XHost := Req.GetHeaderValue('X-Forwarded-Host');
    if (Result.Host = '') and (XHost.Trim <> '') then
      Result.Host := FirstCsvToken(XHost);

    XPort := Req.GetHeaderValue('X-Forwarded-Port');
    Port := StrToIntDef(FirstCsvToken(XPort), 0);
    if (Result.Port = 0) and (Port > 0) then
      Result.Port := Port;
  end;

  // Em último caso, obter Host do cabeçalho "Host" (se não houver cabeçalho de proxy)
  if Result.Host = '' then
  begin
    Host := Req.GetHeaderValue('Host');
    if Host <> '' then
      SplitHostPort(Host, Result.Host, Result.Port);
  end;
end;

end.

Objetivo: Você obtém, a partir de cada requisição, uma visão consistente de ClientIP, Proto e Host assim como uma BaseUrl. Essas informações podem ser usadas centralmente para registro, decisões de segurança (p.ex. lista de permissão de IP) e geração de links.

Por que a lista de Trust-Proxy é necessária: Sem a verificação de confiança, um atacante poderia alcançar diretamente a sua porta Delphi (misconfiguração, roteamento interno, VPN) e simplesmente enviar X-Forwarded-For: 127.0.0.1. Isso tornaria audit-trails, rate limits ou endpoints “apenas internos” vulneráveis. Confie em cabeçalhos Forwarded apenas se o peer direto (RemoteIP) for um proxy que você controla (p.ex. 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).

Atenção: IPv6 sem colchetes não é inequívoco na notação Host:port. No cabeçalho HTTP Host, IPv6 normalmente é indicado entre []; siga esse formato. Para intervalos IP complexos (CIDR) será necessário estender a lista de confiança (p.ex. através de um parsing IP real).

Integração em WebBroker/Horse/RAD Server: onde o código „andockt“

No WebBroker (TWebRequest) os cabeçalhos normalmente chegam via ContentFields ou GetFieldByName, a Remote-IP depende do backend do servidor. No Horse (ou outros frameworks HTTP) geralmente existem Req.Headers e uma propriedade de Remote-IP. O princípio importante é: RemoteIP deve ser a contraparte TCP, não um valor qualquer de cabeçalho.

Prática recomendada: gere na inicialização do serviço uma TTrustedProxyList a partir da configuração (INI/ENV), p.ex. “127.0.0.1” para setups locais com nginx ou o IP do seu load balancer. Em seguida chame ResolveForwardedInfo por requisição e grave os campos no seu registro estruturado (JSON-Log, Syslog ou Windows Event Log).

Debugging em operação: assim você encontra erros em minutos em vez de horas

Quando requisições parecem “estranhas”, raramente é culpa do Delphi-HTTP em si; normalmente é uma combinação de cabeçalhos de proxy, lógica de redirect e timeouts. Três verificações de debug que valem a pena no dia a dia:

  1. Dump de cabeçalhos (seletivo): Registre em 4xx/5xx adicionalmente Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent e Request-URI. Mas somente em caso de erro — caso contrário o log fica caro e confuso.
  2. Verificar Base-URL: Se redirects ou URLs de callback falharem, registre ForwardedInfo.BaseUrl. Muitos erros ficam imediatamente visíveis (“http://127.0.0.1” em vez de “https://api…”).
  3. Correlação de timeouts: Um 504 do proxy não é o mesmo que um timeout do Delphi. nginx proxy_read_timeout e os timeouts Idle-/Read do lado do Delphi devem estar alinhados.

Casos-limite: WebSockets, streaming e requisições grandes

WebSockets atrás do nginx

Para WebSockets o nginx precisa ter Upgrade e Connection configurados corretamente. Além disso, o backend não pode fechar “cedo”. No lado do Delphi é relevante que sua componente WebSocket (ou um endpoint SSE/streaming) consiga lidar com reverse proxies e que Heartbeats/Keep-Alives estejam implementados corretamente.

Uploads grandes e erros 413

Um clássico: Delphi aceita um upload, mas o nginx bloqueia antes com 413 Request Entity Too Large. Controle isso explicitamente via client_max_body_size e ajuste os limites de requisição no lado do Delphi. Para soluções de software próximas ao processo que lidam com documentos ou imagens, isso não é um caso excepcional, mas operação normal.

Offloading de HTTPS e „Secure Cookies“

Se o seu Delphi-Service define cookies de sessão, estes normalmente precisam ser marcados como Secure em HTTPS externo. Se a sua aplicação faz isso frequentemente depende de ela “saber” que a requisição original foi HTTPS. Exatamente aqui ajuda a avaliação consistente de X-Forwarded-Proto/Forwarded.

Quando vale a pena o esforço – e onde ele pode falhar

A abordagem mostrada compensa sempre que o Delphi-Service não estiver mais “exposto” na LAN, mas fizer parte de uma borda de produção: múltiplos domínios, interfaces SSO/SAML, APIs públicas, suporte a múltiplos tenants ou requisitos de auditoria mais rígidos. Ela falha onde se confia cegamente nos cabeçalhos Forwarded ou onde as topologias de proxy não estão documentadas (várias camadas de ingress, Cloud-LB mais nginx mais sidecar). Então o IP do cliente e o esquema rapidamente se tornam “qualquer coisa”.

Um limite claro: se precisar de regras de confiança complexas (CIDR, redes IPv6, IPs dinâmicos de LB), deve ampliar a verificação de confiança (parsing real de IP, máscaras de rede) ou projetar a infraestrutura de modo que apenas um proxy definido consiga alcançar a porta do Delphi (Firewall/Security Groups). No fim das contas essa costuma ser a decisão operacional mais robusta.

Conclusão: Operar um Reverse Proxy com nginx e Delphi corretamente significa “fazer o Forwarded direito”

Um Reverse Proxy com nginx é um componente padrão sólido para Delphi-REST-Server – mas somente o tratamento correto de Forwarded e X-Forwarded-* torna o ambiente estável em operação. O núcleo é simples: aceitar headers apenas de proxies confiáveis, derivar Client-IP/Scheme/Host de forma consistente e aplicar essa base em redirecionamentos, logging e verificações de segurança. Com o snippet acima você tem uma base limpa, compatível com legado, que se integra em WebBroker, Horse ou em seus próprios servidores HTTP.

Se pretende consolidar um backend Delphi existente atrás do nginx ou modernizar em direção a Delphi REST-API e REST-Server com uma linha operacional clara, uma revisão técnica da cadeia de proxies e da avaliação dos headers costuma ser a alavanca mais rápida. Contacte Net-Base para uma breve classificação técnica.

No contexto técnico, o Nginx Reverse Proxy e os cabeçalhos Forwarded também desempenham um papel importante quando integrações, fluxos de dados e evolução precisam funcionar em conjunto de forma consistente.

Discutir projeto ou iniciativa de modernização com Net-Base.

Partilhar publicação

Compartilhar esta publicação diretamente

LinkedIn, X, XING, Facebook, WhatsApp e e‑mail estão imediatamente disponíveis. Para o Instagram, preparamos o link e um texto curto de imediato.

E-mail

O Instagram abre numa nova aba. O link e o texto curto são copiados previamente para a área de transferência.