Net-Base Magazine

23.05.2026

Reverse proxy avec nginx et Delphi : gestion rigoureuse des en-têtes Forwarded, IP client réelle et bases d’URL robustes

Lorsque des serveurs Delphi-REST fonctionnent derrière nginx, l'adresse IP du client, la détection HTTPS et les URL absolues sont souvent altérées. Cet extrait de code source montre une gestion robuste des en-têtes Forwarded / X-Forwarded (y compris une liste de proxies de confiance), des paramètres nginx typiques et des indications de débogage pour l'exploitation.

23.05.2026

Un Reverse Proxy avec nginx et Delphi n’est en pratique généralement pas un « nice to have », mais la séparation nette entre la bordure Internet et l’application : terminaison TLS (HTTPS-Offloading), règles centrales d’en-têtes/CORS, limites de débit, logs unifiés, déploiements Blue/Green ou simplement l’hébergement de plusieurs services sous un même domaine. Ce qui est souvent sous-estimé : dès que nginx est « devant », le serveur Delphi ne voit plus que l’IP du proxy, souvent seulement « http » au lieu de « https » et génère des liens absolus incorrects (redirections, URL de callback, URL du serveur OpenAPI). Ce sont précisément ces trois points qui entraînent ensuite du temps de débogage en exploitation.

Cet extrait de code montre un modèle robuste pour traiter proprement dans Delphi les en-têtes Forwarded et X-Forwarded-* – y compris une Trust-Proxy-Liste (important contre le spoofing d’en-têtes) et une Request-Base-URL cohérente. S’y ajoutent des configurations nginx adaptées à la pratique et des remarques sur des cas limites comme les WebSockets, les gros uploads et les timeouts.

Pourquoi les configurations de reverse proxy « perturbent » les serveurs Delphi

nginx communique en tant que Reverse Proxy avec le service Delphi typiquement en clair (HTTP) sur le réseau interne ou en localhost, tandis que le client arrive par HTTPS depuis l’extérieur. Sans en-têtes supplémentaires, Delphi n’a aucune information sur :

  • Schéma d’origine (https vs. http) — pertinent pour les redirections et les URL absolues.
  • Hôte d’origine (domaine client, port) — pertinent pour les configurations multi-tenant, le CORS et les URLs de callback.
  • IP client d’origine — pertinent pour l’audit, les limites de débit, les vérifications géographiques et les analyses de sécurité.

nginx peut véhiculer ces informations via des en-têtes. Courants : X-Forwarded-For, X-Forwarded-Proto et X-Forwarded-Host ; standardisé, on trouve aussi l’en-tête RFC Forwarded. Important : ces en-têtes ne sont pas automatiquement fiables du point de vue de l’application, car un client peut les envoyer lui‑même — ils ne deviennent fiables que s’ils proviennent d’un proxy connu.

Configuration nginx : les en-têtes proxy minimaux recommandés

Un point de départ solide (HTTP/1.1, Keep-Alive, Upgrade pour WebSockets) ressemble à ceci. L’extrait est volontairement succinct ; vous ajouterez selon l’environnement HSTS, des limites de débit et des logs d’accès.

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;
  }
}

But : L’application reçoit de manière fiable le Host, l’IP client et le schéma. Contrainte : $proxy_add_x_forwarded_for ajoute l’IP du proxy actuel à une chaîne éventuellement existante ; c’est utile pour les configurations multi-proxy, mais cela rend d’autant plus importante une évaluation correcte côté Delphi. Écueil : si vous ne définissez pas l’en-tête Host dans nginx, Delphi peut n’apercevoir que l’upstream-host (127.0.0.1), ce qui casse les redirections et les contrôles d’origine.

Delphi Extrait de code source : évaluer de manière robuste Forwarded/X-Forwarded (avec liste de proxies de confiance)

Le code suivant est délibérément indépendant du framework : il opère sur une interface minimale (Header + RemoteIP) et peut être adapté à WebBroker, RAD Server ou Horse. Points clés :

  • Priorité : RFC Forwarded (si présent) avant X-Forwarded-*.
  • Confiance : n’évaluer les en-têtes Forwarded que si le pair direct (RemoteIP) est un proxy connu.
  • Analyse : prendre en compte IPv6, les guillemets, les ports et les chaînes dans X-Forwarded-For.
  • Sortie : une Base-URL que vous pouvez utiliser pour des liens absolus, des redirections ou OpenAPI.

unit Net-Base.ProxyForwarding;

interface

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

type
// Interface d’adaptateur minimal : implémentez-le pour WebBroker/Horse/etc.
IHeaderReader = interface
[‚{C2D2E5B9-2C2E-4D37-9D73-3CDB5A7E7EEA}‘]
function GetHeaderValue(const AName: string): string;
function GetRemoteIP: string; // point de terminaison TCP distant (généralement nginx)
end;

TForwardedInfo = record
ClientIP: string;
Proto: string; // http/https
Host: string;
Port: Integer;
function EffectiveScheme: string;
function EffectiveHostPort: string;
function BaseUrl: string; // p. 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 peut être ‚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 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 (Attention : non fiable pour IPv6 sans []) 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
// Exemple : Forwarded: for=203.0.113.43;proto=https;host=api.example.com
Parts: TArray;
I: Integer;
KV: TArray;
K, V: string;
FirstElement: string;
begin
Result := False;
ClientIP := “;
Proto := “;
Host := “;

if ForwardedValue.Trim = “ then
Exit;

// Plusieurs éléments sont séparés par des virgules ; nous prenons le premier (le plus proche du 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 peut être une IP ou ‚unknown‘ ; l’IPv6 peut être entre []
V := V.Trim;
if SameText(V, ‚unknown‘) then
Continue;
// for=1.2.3.4:5678 peut arriver
if V.Contains(‚:‘) and (not V.StartsWith(‚[‚)) then
V := FirstCsvToken(V); // par précaution
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.Create;
end;

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

class function TTrustedProxyList.NormalizeIp(const AIP: string): string;
begin
// Pour une vraie normalisation IPv6, il faudrait parser l’IP ; ici, choix délibérément pragmatique.
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; // Repli : point de terminaison TCP direct

// Nous n’interprétons les en-têtes Forwarded que si le point de terminaison direct est un proxy connu.
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-* comme repli/complément
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;

// En dernier recours, prendre le Host depuis l’en-tête ‚Host‘ (si aucun en-tête proxy présent)
if Result.Host = “ then
begin
Host := Req.GetHeaderValue(‚Host‘);
if Host <> “ then
SplitHostPort(Host, Result.Host, Result.Port);
end;
end;

end.

Objectif : Vous obtenez, pour chaque requête, une vue cohérente de ClientIP, Proto et Host ainsi que d’une BaseUrl. Ces informations peuvent être utilisées de manière centralisée pour le logging, les décisions de sécurité (p. ex. liste d’autorisation d’IP) et la génération de liens.

Pourquoi la liste des proxies de confiance est nécessaire : Sans vérification de confiance, un attaquant pourrait atteindre directement votre port Delphi (mauvaise configuration, routage interne, VPN) et simplement envoyer X-Forwarded-For: 127.0.0.1. Les trails d’audit, les limites de taux ou les endpoints « réservés à l’interne » deviendraient alors vulnérables. Ne faites confiance aux en-têtes Forwarded que si le pair direct (RemoteIP) est un proxy que vous contrôlez (p. ex. 127.0.0.1, IP du load balancer, Kubernetes-Ingress).

Pièges : IPv6 sans crochets n’est pas univoque dans la notation Host:port. Dans l’en-tête HTTP-Host, IPv6 est normalement noté entre [] ; respectez cette convention. Pour des plages d’IP complexes (CIDR), il vous faudra étendre la liste de confiance (p. ex. via un véritable parsing d’adresses IP).

Intégration dans WebBroker/Horse/RAD Server : où le code « andockt »

Dans WebBroker (TWebRequest), les en-têtes arrivent typiquement via ContentFields ou GetFieldByName, la Remote-IP dépend du backend serveur. Dans Horse (ou d’autres frameworks HTTP) il existe en général Req.Headers et une propriété Remote-IP. L’essentiel est le principe : RemoteIP doit être la contrepartie TCP, pas une valeur d’en-tête quelconque.

Pratique éprouvée : créez au démarrage du service une TTrustedProxyList à partir de la configuration (INI/ENV), p. ex. « 127.0.0.1 » pour des setups nginx locaux ou l’IP de votre load balancer. Ensuite, appelez ResolveForwardedInfo pour chaque requête et écrivez les champs dans votre logging structuré (JSON-Log, Syslog ou Windows Event Log).

Débogage en exploitation : comment trouver des erreurs en minutes plutôt qu’en heures

Quand des requêtes semblent « bizarres », le problème vient rarement de Delphi-HTTP lui-même, mais d’une combinaison d’en-têtes de proxy, de logique de redirection et de timeouts. Trois vérifications de debug utiles au quotidien :

  1. Dump des en-têtes (ciblé) : loggez en cas de 4xx/5xx en plus Host, Forwarded, X-Forwarded-For, X-Forwarded-Proto, User-Agent et Request-URI. Mais uniquement en cas d’erreur – sinon les logs deviennent coûteux et illisibles.
  2. Vérifier la Base-URL : si des redirections ou des URLs de callback échouent, loggez ForwardedInfo.BaseUrl. Beaucoup d’erreurs deviennent immédiatement visibles («http://127.0.0.1» au lieu de «https://api…»).
  3. Corrélation des timeouts : un 504 renvoyé par le proxy n’est pas la même chose qu’un timeout Delphi. nginx proxy_read_timeout et les timeouts Idle/Read côté Delphi doivent être cohérents.

Cas particuliers : WebSockets, streaming et requêtes volumineuses

WebSockets derrière nginx

Pour les WebSockets, nginx a besoin que Upgrade et Connection soient corrects. De plus, le backend ne doit pas fermer « trop tôt ». Côté Delphi, il est important que votre composant WebSocket (ou un endpoint SSE/Streaming) sache gérer les reverse proxies et que les heartbeats/keep-alives soient correctement implémentés.

Téléversements volumineux et erreurs 413

Un classique : Delphi accepte un upload, mais nginx bloque en amont avec 413 Request Entity Too Large. Contrôlez cela explicitement via client_max_body_size et ajustez côté Delphi les limites de requête. Pour des solutions logicielles proches du processus traitant des documents ou des images, ce n’est pas un cas particulier mais l’opération normale.

HTTPS-Offloading und „Secure Cookies“

Si votre service Delphi définit des cookies de session, ceux-ci doivent en général être marqués comme Secure lorsqu’HTTPS est géré en externe. Le fait que votre application le fasse dépend souvent de sa « connaissance » que la requête initiale était en HTTPS. C’est précisément là que l’évaluation cohérente de X-Forwarded-Proto/Forwarded apporte son aide.

Quand l’effort en vaut la peine – et où il peut basculer

L’approche présentée est pertinente chaque fois que le service Delphi n’est plus « nu » dans le LAN, mais fait partie d’une bordure productive : plusieurs domaines, interfaces SSO/SAML, APIs publiques, capacité multi-locataire ou exigences d’audit plus strictes. Elle devient fragile lorsqu’on fait confiance aveuglément aux en‑têtes Forwarded ou que les topologies de proxy ne sont pas documentées (plusieurs niveaux d’Ingress, Cloud-LB plus nginx plus sidecar). Dans ces cas, l’adresse IP du client et le schéma deviennent rapidement peu fiables.

Frontière claire : si vous avez besoin de règles de confiance complexes (CIDR, réseaux IPv6, adresses IP d’un LB dynamiques), vous devriez renforcer la vérification de confiance (vérification réelle des adresses IP, masques réseau) ou concevoir l’infrastructure de sorte qu’un proxy défini soit le seul à pouvoir atteindre le port Delphi (pare-feu/Security Groups). C’est souvent, en pratique, la décision opérationnelle la plus robuste.

Conclusion : exploiter proprement un reverse proxy nginx avec Delphi signifie « bien traiter Forwarded »

Un reverse proxy nginx est un bon composant standard pour des Delphi-REST-Server – mais c’est le traitement correct des en‑têtes Forwarded et X-Forwarded-* qui rend le dispositif stable en exploitation. Le principe est simple : n’accepter les en‑têtes que des proxies de confiance, dériver de façon cohérente Client-IP/Scheme/Host et appliquer cette base de façon homogène dans les redirections, le logging et les contrôles de sécurité. Avec le snippet ci‑dessous, vous disposez d’un socle propre, compatible avec des systèmes hérités, qui s’intègre dans WebBroker, Horse ou vos propres serveurs HTTP.

Si vous consolidez un backend Delphi existant derrière nginx ou si vous souhaitez moderniser vers une API Delphi REST et REST-Server avec une ligne d’exploitation claire, une revue technique de la chaîne de proxies et du traitement des en‑têtes est souvent le levier le plus rapide. Contactez Net-Base pour un bref cadrage technique.

Dans le contexte fonctionnel, Nginx Reverse Proxy et les en‑têtes Forwarded jouent aussi un rôle important lorsque les intégrations, les flux de données et l’évolution doivent s’articuler proprement.

Discuter d’un projet ou d’une modernisation avec Net-Base.

Partager l'article

Partager directement cette publication

LinkedIn, X, XING, Facebook, WhatsApp et e-mail sont immédiatement disponibles. Pour Instagram, nous préparons directement le lien et un court texte.

Courriel

Instagram s'ouvre dans un nouvel onglet. Le lien et le court texte sont préalablement copiés dans le presse-papiers.