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.
# (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.
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:
- 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.
- 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…”).
- 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.