Een Reverse Proxy met nginx en Delphi is in de praktijk meestal geen „nice to have“, maar de duidelijke scheiding tussen internetrand en applicatie: TLS-terminatie (HTTPS-Offloading), centrale Header-/CORS-regels, Rate-Limits, uniforme logs, Blue/Green-Rollouts of gewoon het hosten van meerdere services onder één domein. Wat daarbij vaak onderschat wordt: zodra nginx „ervoor“ zit, ziet de Delphi-server alleen nog het proxy-IP, vaak alleen nog „http“ in plaats van „https“ en genereert hij foutieve absolute links (Redirects, Callback-URLs, OpenAPI-Server-URL). Juist deze drie punten zorgen later voor extra debugging-tijd in productie.
Dit broncodefragment toont een robuust patroon hoe u in Delphi Forwarded respectievelijk X-Forwarded-* correct kunt uitlezen – inclusief de Trust-Proxy-Liste (belangrijk tegen Header-Spoofing) en een consistente Request-Base-URL. Daarnaast zijn er praktijkgerichte nginx-configuraties en aanwijzingen voor randgevallen zoals WebSockets, grote uploads en timeouts.
Waarom Reverse-Proxy-opstellingen Delphi-servers „verwarren“
nginx spreekt als Reverse Proxy met het Delphi-service doorgaans onversleuteld (HTTP) in het interne netwerk of op localhost, terwijl de client van buiten via HTTPS komt. Zonder aanvullende headers weet Delphi niets van:
- Original-Schema (https vs. http) – relevant voor Redirects en absolute URL’s.
- Original-Host (klantspecifieke Domain, Port) – relevant voor Multi-Tenant-Setups, CORS en Callback-URLs.
- Original-Client-IP – relevant voor Audit, Rate-Limits, Geo-Checks en Security-Auswertungen.
nginx kan deze informatie via headers transporteren. Gebruikelijk zijn X-Forwarded-For, X-Forwarded-Proto en X-Forwarded-Host; gestandaardiseerd is daarnaast de RFC-header Forwarded. Belangrijk: deze headers zijn vanuit het perspectief van de applicatie niet automatisch te vertrouwen, omdat een client ze zelf kan sturen – ze worden pas betrouwbaar als ze afkomstig zijn van een bekende proxy.
nginx-Konfiguration: die minimal sinnvollen Proxy-Header
Een solide startpunt (HTTP/1.1, Keep-Alive, Upgrade voor WebSockets) ziet er zo uit. Het snippet is bewust beknopt gehouden; u voegt afhankelijk van de omgeving HSTS, Rate-Limits en Access-Logs toe.
# (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;
}
}
Doel: De applicatie krijgt Host, client-IP en schema betrouwbaar doorgegeven. Randvoorwaarde: $proxy_add_x_forwarded_for hangt de huidige proxy-IP aan een eventuele bestaande keten; dat is goed voor multi-proxy-opstellingen, maar maakt de correcte evaluatie op Delphi-zijde des te belangrijker. Valkuil: Als u in nginx de Host-header niet zet, ziet Delphi mogelijk alleen de upstream-host (127.0.0.1), wat redirects en origin-checks kan breken.
Delphi Source-fragment: Forwarded/X-Forwarded robuust evalueren (met Trust-Proxy-lijst)
De volgende code is bewust framework-neutraal gehouden: hij werkt tegen een minimaal interface (Header + RemoteIP) en is aanpasbaar voor WebBroker, RAD Server of Horse. Kernpunten:
- Prioriteit: RFC Forwarded (indien aanwezig) vóór X-Forwarded-*.
- Trust: Forwarded-headers alleen verwerken als de directe peer (RemoteIP) een bekende proxy is.
- Parsing: rekening houden met IPv6, quotes, poorten en ketens in X-Forwarded-For.
- Output: een base-URL die u kunt gebruiken voor absolute links, redirects of OpenAPI.
unit Net-Base.ProxyForwarding;
interface
uses
System.SysUtils, System.Classes, System.Generics.Collections, System.NetEncoding;
type
// Minimaal adapterinterface: implementeer dit voor WebBroker/Horse/etc.
IHeaderReader = interface
['{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}']
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // directe TCP-tegenpartij (meestal nginx)
end;
TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // bijv. 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 kan "client, proxy1, proxy2" zijn
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 (Let op: bij naakte IPv6 zonder [] niet betrouwbaar)
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
// Voorbeeld: 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;
// Meerdere elementen zijn met komma's gescheiden; we nemen het eerste (het dichtst bij de 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 kan een IP of "unknown" zijn; IPv6 kan tussen [] staan
V := V.Trim;
if SameText(V, 'unknown') then
Continue;
// for=1.2.3.4:5678 komt voor
if V.Contains(':') and (not V.StartsWith('[')) then
V := FirstCsvToken(V); // defensiv
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
// Voor echte IPv6-normalisatie zou je IP-parsing nodig hebben; hier bewust pragmatisch.
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: directe tegenpartij
// Alleen als de directe tegenpartij een bekende proxy is, verwerken we 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-* als fallback/aanvulling
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;
// In noodgevallen Host uit de "Host"-header nemen (als er geen proxy-header is)
if Result.Host = '' then
begin
Host := Req.GetHeaderValue('Host');
if Host <> '' then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;
end.
Doel: U krijgt uit elk request een consistente weergave van ClientIP, Proto en Host en een BaseUrl. Deze informatie kunt u centraal gebruiken voor logging, beveiligingsbeslissingen (bijv. IP-allowlist) en linkgeneratie.
Waarom de Trust-Proxy-lijst nodig is: Zonder trust-controle zou een aanvaller direct uw Delphi-poort kunnen bereiken (foutconfiguratie, intern routing, VPN) en eenvoudig X-Forwarded-For: 127.0.0.1 sturen. Daarmee zouden audit-trails, rate-limits of „alleen intern toegestaan“-endpoints kwetsbaar zijn. Vertrouw Forwarded-headers alleen als de directe peer (RemoteIP) een proxy is die u controleert (bijv. 127.0.0.1, Load-Balancer-IP, Kubernetes-Ingress).
Valkuilen: IPv6 zonder vierkante haken is in Host:port-notatie niet eenduidig. In de HTTP-Host-header wordt IPv6 normaal gesproken in [] genoteerd; houd u daaraan. Voor complexe IP-ranges (CIDR) moet u de Trust-Proxy-lijst uitbreiden (bijv. door echte IP-parsing).
Integratie in WebBroker/Horse/RAD Server: waar de code „aankoppelt“
In WebBroker (TWebRequest) komen headers typisch via ContentFields of GetFieldByName, Remote-IP afhankelijk van het server-backend. In Horse (of andere HTTP-frameworks) is er meestal Req.Headers en een Remote-IP-eigenschap. Belangrijk is het principe: RemoteIP moet de TCP-tegenpartij zijn, niet een willekeurige headerwaarde.
In de praktijk aanbevolen: Maak bij het opstarten van de service een TTrustedProxyList uit configuratie (INI/ENV), bijv. „127.0.0.1“ voor lokale nginx-setups of het IP-adres van uw Load Balancer. Roep vervolgens ResolveForwardedInfo per request aan en schrijf de velden in uw gestructureerde logging (JSON-log, Syslog of Windows Event Log).
Debugging in productie: zo vindt u fouten in minuten in plaats van uren
Als requests „vreemd“ lijken, ligt het zelden aan Delphi-HTTP zelf, maar aan een combinatie van proxy-headers, redirect-logica en timeouts. Drie debug-checks die zich in de dagelijkse praktijk bewijzen:
- Header-dump (gericht): Log bij 4xx/5xx aanvullend Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent en Request-URI. Maar alleen bij fouten – anders wordt de log duur en onoverzichtelijk.
- Base-URL controleren: Als redirects of callback-URL’s mislukken, log ForwardedInfo.BaseUrl. Veel fouten zijn direct zichtbaar („http://127.0.0.1“ in plaats van „https://api…“).
- Timeout-correlatie: Een 504 van de proxy is niet hetzelfde als een Delphi-timeout. nginx proxy_read_timeout en Delphi-zijdige Idle-/Read-Timeouts moeten op elkaar afgestemd zijn.
Randgevallen: WebSockets, streaming en grote requests
WebSockets achter nginx
Voor WebSockets heeft nginx correcte Upgrade en Connection nodig. Daarnaast mag het backend niet „te vroeg“ sluiten. Aan Delphi-zijde is relevant dat uw WebSocket-component (of een SSE/Streaming-endpoint) met reverse proxies kan omgaan en Heartbeats/Keep-Alives netjes zijn geïmplementeerd.
Grote uploads en 413-fouten
Een klassieker: Delphi accepteert een upload, maar nginx blokkeert vooraf met 413 Request Entity Too Large. Beheer dit expliciet via client_max_body_size en pas Delphi-zijdig request-limieten aan. Voor procesgebonden softwareoplossingen met documenten- of beelddata is dit geen uitzondering, maar normale operatie.
HTTPS-Offloading und „Secure Cookies“
Als uw Delphi-service sessiecookies zet, moeten deze bij extern HTTPS in de regel als Secure gemarkeerd zijn. Of uw applicatie dat doet, hangt vaak af van of zij „weet“ dat het oorspronkelijke verzoek HTTPS was. Juist hierbij helpt de consistente evaluatie van X-Forwarded-Proto/Forwarded.
Wanneer de moeite loont – en waar het kan kantelen
De getoonde aanpak loont telkens wanneer de Delphi-service niet langer „bloot“ in het LAN draait, maar deel uitmaakt van een productieve rand: meerdere domeinen, SSO/SAML-interfaces, Public APIs, multi-tenant-ondersteuning of strengere auditvereisten. Het kan misgaan waar men Forwarded-headers blind vertrouwt of proxytopologieën niet documenteert (meerdere Ingress-lagen, Cloud-LB plus nginx plus Sidecar). Dan worden Client-IP en schema snel onbetrouwbaar.
Een duidelijke grens: als u complexe trust-regels nodig heeft (CIDR, IPv6-netten, dynamische LB-IPs), moet u de trust-controle uitbreiden (echt IP-parsing, netmaskers) of de infrastructuur zodanig inrichten dat slechts één gedefinieerde proxy de Delphi-poort kan bereiken (Firewall/Security Groups). Dat is uiteindelijk meestal de robuustere operationele keuze.
Conclusie: Een reverse proxy met nginx en Delphi goed beheren betekent „Forwarded juist afhandelen“
Een reverse proxy met nginx is voor Delphi-REST-Server een goede standaardcomponent – maar pas de correcte verwerking van Forwarded en X-Forwarded-* maakt de opstelling in productie stabiel. De kern is simpel: headers alleen van vertrouwde proxies accepteren, Client-IP/Scheme/Host consistent afleiden en deze basis consequent doorvoeren in redirects, logging en security-checks. Met het bovenstaande snippet heeft u daarvoor een schoon, legacy-compatibel fundament waarmee u in WebBroker, Horse of eigen HTTP-servers kunt integreren.
Als u een bestaand Delphi-backend achter nginx wil consolideren of in de richting van Delphi REST-API en REST-Server met een duidelijke operationele lijn wilt moderniseren, is een technische beoordeling van de proxy-keten en de header-evaluatie vaak de snelste hefboom. Neem contact op met Net-Base voor een korte technische inschatting.
In het vaktechnische kader spelen ook Nginx Reverse Proxy en Forwarded-headers een belangrijke rol wanneer integraties, datastromen en verdere ontwikkeling correct op elkaar moeten aansluiten.