A reverse proxy with nginx and Delphi is in practice usually not a “nice to have”, but the clean separation between the internet edge and the application: TLS termination (HTTPS offloading), central header/CORS rules, rate limits, consistent logs, Blue/Green-Rollouts or simply hosting multiple services under one domain. What is often underestimated then: once nginx sits “in front”, the Delphi server only sees the proxy IP, often only “http” instead of “https”, and generates incorrect absolute links (redirects, callback URLs, OpenAPI server URL). These three points in particular later cause debugging time in operation.
This source snippet shows a robust pattern for how to properly evaluate Forwarded and X-Forwarded-* in Delphi – including a trust-proxy list (important against header spoofing) and a consistent request base URL. It also includes practical nginx configurations and notes on edge cases such as WebSockets, large uploads and timeouts.
Why reverse proxy setups „confuse“ Delphi servers
nginx, as a reverse proxy, typically speaks unencrypted (HTTP) to the Delphi service on the internal network or on localhost, while the client connects via HTTPS externally. Without additional headers Delphi knows nothing about:
- Original scheme (https vs. http) – relevant for redirects and absolute URLs.
- Original host (customer-specific domain, port) – relevant for multi-tenant setups, CORS and callback URLs.
- Original client IP – relevant for auditing, rate limits, geo checks and security analysis.
nginx can transport this information via headers. Common are X-Forwarded-For, X-Forwarded-Proto and X-Forwarded-Host; additionally the RFC header Forwarded is standardized. Important: from the application’s perspective these headers are not automatically trustworthy, because a client can send them itself – they only become trustworthy when they originate from a known proxy.
nginx configuration: the minimally sensible proxy headers
A solid starting point (HTTP/1.1, keep-alive, upgrade for WebSockets) looks like this. The snippet is intentionally compact; add HSTS, rate limits and access logs as appropriate for your environment.
# (nginx configuration, not 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, but practical for 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 matching the Delphi backend (long reports/exports)
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# explicitly control large uploads
client_max_body_size 50m;
}
}
Purpose: The application receives Host, client IP and scheme reliably forwarded. Prerequisite: $proxy_add_x_forwarded_for appends the current proxy IP to any existing chain; this is useful for multi-proxy setups, but it makes correct evaluation on Delphi side all the more important. Pitfall: If you do not set the Host header in nginx, Delphi may only see the upstream host (127.0.0.1), which breaks redirects and origin checks.
Delphi source snippet: robust evaluation of Forwarded/X-Forwarded (with trust-proxy list)
The following code is intentionally framework-neutral: it operates against a minimal interface (headers + RemoteIP) and can be adapted to WebBroker, RAD Server or Horse. Key points:
- Priority: RFC Forwarded (if present) before X-Forwarded-*.
- Trust: only evaluate Forwarded headers when the direct peer (RemoteIP) is a known proxy.
- Parsing: account for IPv6, quotes, ports and chains in X-Forwarded-For.
- Output: a base URL you can use for absolute links, redirects or OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minimal adapter interface: implement this for WebBroker/Horse/etc.
IHeaderReader = interface
[‚{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}‘]
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // direct TCP peer (usually nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // e.g. 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 can be ‚client, 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 in []: [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 (Note: not reliable for naked IPv6 without [])
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
// Example: 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;
// Multiple entries are comma-separated; we take the first (closest to the client)
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 can be an IP or ‚unknown‘; IPv6 may be in []
V := V.Trim;
if SameText(V, ‚unknown‘) then
Continue;
// for=1.2.3.4:5678 can occur
if V.Contains(‚:‘) and (not V.StartsWith(‚[‚)) then
V := FirstCsvToken(V); // defensive
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
// Real IPv6 normalization would require IP parsing; this is deliberately pragmatic.
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: direct peer
// Only if the direct peer is a known proxy do we evaluate Forwarded headers.
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-* as fallback/supplement
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;
// As a last resort, take Host from the ‚Host‘ header (if no proxy header is present)
if Result.Host = “ then
begin
Host := Req.GetHeaderValue(‚Host‘);
if Host <> “ then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Purpose: From each request you get a consistent view of ClientIP, Proto and Host as well as a BaseUrl. You can use this information centrally for logging, security decisions (e.g. IP allowlist) and link generation.
Why the trust-proxy list is necessary: Without a trust check an attacker could reach your Delphi port directly (misconfiguration, internal routing, VPN) and simply send X-Forwarded-For: 127.0.0.1. That would make audit trails, rate limits or „internal-only“ endpoints vulnerable. Only trust forwarded headers when the direct peer (RemoteIP) is a proxy you control (e.g. 127.0.0.1, load balancer IP, Kubernetes ingress).
Pitfalls: IPv6 without square brackets is ambiguous in host:port notation. In the HTTP Host header IPv6 is normally written in []; stick to that. For complex IP ranges (CIDR) you would need to extend the trust list (e.g. by real IP parsing).
Integration in WebBroker/Horse/RAD Server: where the code „docks“
In WebBroker (TWebRequest) headers typically arrive via ContentFields or GetFieldByName, remote IP depends on the server backend. In Horse (or other HTTP frameworks) there is usually Req.Headers and a remote-IP property. The important principle: RemoteIP must be the TCP peer, not any header value.
Practically proven: create a TTrustedProxyList from configuration (INI/ENV) at service start, e.g. „127.0.0.1“ for local nginx setups or the IP of your load balancer. Then call ResolveForwardedInfo per request and write the fields into your structured logging (JSON log, syslog or Windows Event Log).
Debugging in production: find faults in minutes instead of hours
When requests appear „odd“ it is rarely Delphi-HTTP itself, but a combination of proxy headers, redirect logic and timeouts. Three debug checks that pay off in daily operation:
- Targeted header dump: Log Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent and Request-URI additionally on 4xx/5xx. But only on errors—otherwise the log becomes expensive and noisy.
- Check the base URL: If redirects or callback URLs fail, log ForwardedInfo.BaseUrl. Many problems are immediately obvious („http://127.0.0.1“ instead of „https://api…“).
- Timeout correlation: A 504 from the proxy is not the same as a Delphi timeout. nginx proxy_read_timeout and Delphi-side idle/read timeouts must match.
Edge cases: WebSockets, streaming and large requests
WebSockets behind nginx
For WebSockets nginx needs Upgrade and Connection set correctly. Additionally, the backend must not close „too early.“ On the Delphi side it is important that your WebSocket component (or an SSE/streaming endpoint) can handle reverse proxies and that heartbeats/keep-alives are implemented cleanly.
Large uploads and 413 errors
A classic: Delphi accepts an upload, but nginx blocks it earlier with 413 Request Entity Too Large. Control this explicitly via client_max_body_size and adjust request limits on the Delphi side. For process-near software solutions handling documents or image data this is not an edge case but normal operation.
HTTPS offloading and „secure cookies“
If your Delphi service sets session cookies, these generally need to be marked as Secure when external HTTPS is used. Whether your application does this often depends on whether it „knows“ that the original request was HTTPS. This is exactly where consistent evaluation of X-Forwarded-Proto/Forwarded helps.
When the effort pays off – and where it can fail
The approach shown pays off whenever the Delphi service no longer runs „naked“ in the LAN but is part of a production edge: multiple domains, SSO/SAML interfaces, public APIs, multi-tenancy or stricter audit requirements. It fails where Forwarded headers are trusted blindly or proxy topologies are undocumented (multiple ingress stages, Cloud-LB plus nginx plus sidecar). Then client IP and scheme quickly become „something“.
A clear boundary: If you need complex trust rules (CIDR, IPv6 networks, dynamic LB IPs), you should extend the trust check (real IP parsing, netmasks) or design the infrastructure so that only a defined proxy can reach the Delphi port (Firewall/Security Groups). That is usually the more robust operations decision in the end.
Conclusion: Operating a reverse proxy with nginx and Delphi properly means „handling Forwarded correctly“
A reverse proxy with nginx is a solid standard component for Delphi-REST-server – but it is the correct handling of Forwarded and X-Forwarded-* that makes the setup stable in operation. The core is simple: accept headers only from trusted proxies, derive client IP/scheme/host consistently and carry that base through redirects, logging and security checks. With the snippet above you have a clean, legacy-compatible foundation that can be integrated into WebBroker, Horse or your own HTTP servers.
If you are consolidating an existing Delphi backend behind nginx or modernizing towards Delphi REST-API and REST-server with a clear operational boundary, a technical review of the proxy chain and header evaluation is often the quickest lever. Contact Net-Base for a brief technical assessment.
In the professional context, Nginx reverse proxy and Forwarded headers also play an important role when integrations, data flows and further development need to work together cleanly.
Discuss a project or modernization initiative with Net-Base.