Contenu mixte derrière un proxy : pourquoi votre appli produit des liens http://, et le correctif par cadriciel.
Un proxy qui termine le HTTPS parle à votre appli en HTTP simple : l’appli produit donc des URL http:// pour ses ressources, ses redirections et ses rappels. Le navigateur bloque alors scripts et feuilles de style comme contenu mixte. Le correctif tient en un paramètre qui fait confiance à l’en-tête X-Forwarded-Proto, et seulement depuis l’adresse du proxy.
Faits vérifiés le
Pourquoi un site HTTPS se retrouve avec des liens http://
Le navigateur du visiteur se connecte au proxy en HTTPS. Le proxy déchiffre la requête et la transmet à votre appli sur un port local, en HTTP. Du côté de l’appli, rien n’indique que la requête d’origine était sécurisée. Un cadriciel qui construit des URL absolues à partir de la requête reçue (aides pour les ressources, appels url() et redirect(), URL de rappel OAuth, liens du plan du site) écrit http://.
Les proxys transmettent les détails d’origine dans des en-têtes. Votre cadriciel les ignore tant que vous ne lui dites pas quel proxy croire, parce que n’importe quel client peut envoyer son propre en-tête X-Forwarded-Proto.
Ce que le navigateur fait du contenu mixte
Les navigateurs séparent les ressources http:// d’une page HTTPS en deux groupes :
- Une boucle de redirection (
ERR_TOO_MANY_REDIRECTS) est le même bogue vu de l’autre côté : l’appli force le HTTPS, voit du HTTP à chaque requête et redirige encore. - Des connexions qui ne tiennent pas : un témoin de session marqué
securen’est jamais défini quand le cadriciel croit la connexion en HTTP. - Tous les visiteurs ont la même adresse IP, celle du proxy, dans vos journaux et vos limites de débit. C’est
X-Forwarded-Forqui est ignoré.
| Ressource | Ce que fait le navigateur | Ce que vous voyez |
|---|---|---|
<img src>, <audio>, <video>, background-image en CSS † | Passe la requête en https:// automatiquement (sauf si l’hôte est une adresse IP) | En général rien; un avertissement dans la console |
<script src>, <link rel=stylesheet>, <iframe>, fetch(), XHR, polices Web, images srcset † | Bloque la requête | Une page sans styles, des boutons morts, une intégration brisée. La console nomme l’URL http:// bloquée |
† Les détails sur les concurrents proviennent des pages publiées par chaque fournisseur aux dates indiquées; les prix et forfaits changent, vérifiez auprès du fournisseur avant de décider. `<img src>`, `<audio>`, `<video>`, `background-image` en CSS (2026-10-29) · `<script src>`, `<link rel=stylesheet>`, `<iframe>`, `fetch()`, XHR, polices Web, images `srcset` (2026-10-29)
Les en-têtes qu’envoie le proxy de Canner
Sur Canner, Caddy termine le HTTPS et transmet à votre appli sur 127.0.0.1. Voici les en-têtes que reçoit votre appli. Caddy ignore les valeurs X-Forwarded-* envoyées par les clients : ce sont les siennes.
| En-tête | Valeur | Notes |
|---|---|---|
Host † | Le nom d’hôte saisi par le visiteur | Transmis tel quel : votre-projet.canner.app ou votre domaine personnalisé |
X-Forwarded-Proto † | https | Défini par Caddy. Les requêtes en HTTP simple sont redirigées vers le HTTPS avant d’atteindre l’appli |
X-Forwarded-Host † | Identique à Host | Caddy ne réécrit pas Host : vous en avez rarement besoin |
X-Forwarded-For † | L’adresse IP du visiteur | Une seule entrée. La cache optionnelle de Canner la transmet telle quelle |
† Les détails sur les concurrents proviennent des pages publiées par chaque fournisseur aux dates indiquées; les prix et forfaits changent, vérifiez auprès du fournisseur avant de décider. `Host` (2026-10-29) · `X-Forwarded-Proto` (2026-10-29) · `X-Forwarded-Host` (2026-10-29) · `X-Forwarded-For` (2026-10-29)
Le paramètre de chaque cadriciel
Faites confiance à l’adresse du proxy, pas à tout le monde. Sur Canner, cette adresse est l’adresse de bouclage, parce que votre appli écoute sur 127.0.0.1. La dernière colonne indique ce que la commande de démarrage par défaut de Canner règle déjà.
// Express
app.set("trust proxy", "loopback");
# Django (settings.py)
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Flask
from werkzeug.middleware.proxy_fix import ProxyFix
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
# Rails (config/environments/production.rb, Rails 8 default)
config.assume_ssl = true
config.force_ssl = true
// Laravel (bootstrap/app.php)
->withMiddleware(function (Middleware $middleware): void {
$middleware->trustProxies(at: '127.0.0.1');
})
# Spring Boot (application.properties)
server.forward-headers-strategy=native| Cadriciel | Sans le paramètre | Le paramètre | Sur Canner |
|---|---|---|---|
| Express † † | req.protocol vaut http, req.secure est faux, req.ip est le proxy; express-session ne définit pas de témoin secure | app.set('trust proxy', 'loopback') | Nécessaire : Express ne fait confiance à aucun proxy par défaut |
| Django † † | request.is_secure() faux : 403 CSRF, boucles de SECURE_SSL_REDIRECT | SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') | Gunicorn marque déjà https les requêtes de bouclage d’après l’en-tête; définissez-le quand même (détails) |
| Flask † † | url_for(..., _external=True) produit http://; request.remote_addr est le proxy | ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1) de Werkzeug | Gunicorn définit le schéma depuis le bouclage; ajoutez ProxyFix pour l’IP du client |
| FastAPI / uvicorn † | request.url et url_for utilisent http | --proxy-headers avec --forwarded-allow-ips | Réglé : les en-têtes de proxy sont actifs par défaut et acceptés de 127.0.0.1 et ::1 |
| Rails † † | Boucles de redirection de force_ssl, témoins sans secure | config.assume_ssl = true avec config.force_ssl = true | Les deux sont actifs dans un production.rb de Rails 8; ajoutez-les aux applis plus anciennes |
| Laravel † | url(), asset() et @vite produisent http:// : feuilles de style et scripts bloqués | $middleware->trustProxies(at: '127.0.0.1') dans bootstrap/app.php | Nécessaire : Canner lance php artisan serve sur 127.0.0.1 |
| Next.js † † | Les URL absolues que vous construisez (métadonnées, courriels, rappels d’authentification) pointent vers la mauvaise origine | Définissez metadataBase à partir d’une variable d’environnement; pour Auth.js, AUTH_TRUST_HOST=true | Auth.js ne déduit la confiance que sur Vercel et Cloudflare Pages : définissez-la |
| Spring Boot † | Redirections et liens en http avec le port interne | server.forward-headers-strategy=native | Nécessaire : la valeur par défaut est NONE hors des plateformes infonuagiques que Spring détecte |
† Les détails sur les concurrents proviennent des pages publiées par chaque fournisseur aux dates indiquées; les prix et forfaits changent, vérifiez auprès du fournisseur avant de décider. Express (2026-10-29) · Express (2026-10-29) · Django (2026-10-29) · Django (2026-10-29) · Flask (2026-10-29) · Flask (2026-10-29) · FastAPI / uvicorn (2026-10-29) · Rails (2026-10-29) · Rails (2026-10-29) · Laravel (2026-10-29) · Next.js (2026-10-29) · Next.js (2026-10-29) · Spring Boot (2026-10-29)
Ne faites pas confiance à tous les proxys
Des réglages comme trust proxy: true, trustProxies(at: '*') ou --forwarded-allow-ips='*' croient n’importe quel en-tête X-Forwarded-* de quiconque peut joindre l’appli. La documentation d’Express prévient qu’un client peut alors fournir n’importe quelle valeur, à moins que le dernier proxy écrase les en-têtes. Sur une plateforme où votre appli n’accepte de connexions que sur l’adresse de bouclage, faire confiance au bouclage est le réglage exact.
Content-Security-Policy: upgrade-insecure-requests fait réécrire par le navigateur les sous-ressources http:// en https://, y compris celles qui seraient bloquées. Le symptôme disparaît de la page, mais redirections, témoins, courriels et réponses d’API gardent le mauvais schéma. Corrigez d’abord le paramètre du cadriciel.
Vérifiez ce que votre appli croit avoir reçu
Ajoutez une route temporaire qui affiche le schéma, l’hôte et l’IP du client tels que les voit votre cadriciel, déployez, ouvrez-la en HTTPS, puis supprimez-la. La version Express est ci-dessous; chaque cadriciel a son équivalent (request.is_secure() dans Django, request.ssl? dans Rails, $request->secure() dans Laravel). Définissez les valeurs nécessaires avec l’interface en ligne de commande (canner env set) ou le tableau de bord, et lisez la sortie de l’appli dans les journaux.
// A throwaway route: what does the app think the request was?
app.get("/_debug/proto", (req, res) => {
res.json({
protocol: req.protocol, // want "https"
secure: req.secure, // want true
host: req.hostname, // want your public hostname
ip: req.ip, // want the visitor's IP, not 127.0.0.1
xfp: req.get("x-forwarded-proto"),
});
});Quand la cause est ailleurs
Si l’URL http:// est codée en dur (un champ de CMS, une valeur .env comme APP_URL=http://…, une ligne de base de données d’avant le HTTPS), aucun réglage de proxy ne la change. Cherchez http:// dans le code source de la page et corrigez la valeur à sa source. D’autres guides sont dans hébergement par cadriciel.
Questions
Qu’est-ce qui cause les erreurs de contenu mixte sur un site HTTPS?
Une page HTTPS qui charge un script, une feuille de style, un iframe ou une URL fetch() en http://. Derrière un proxy qui termine le TLS, la source habituelle est l’appli elle-même, qui construit des URL http:// faute de faire confiance à X-Forwarded-Proto.
Qu’est-ce que X-Forwarded-Proto?
Un en-tête qu’ajoute le proxy pour dire quel schéma le visiteur a utilisé, https ou http. L’appli derrière reçoit du HTTP simple dans les deux cas : cet en-tête est sa seule façon de le savoir.
Faut-il mettre trust proxy à true dans Express?
Seulement si le proxy devant écrase X-Forwarded-* à chaque requête. Une valeur plus étroite est plus sûre : 'loopback' quand le proxy tourne sur la même machine, ou le nombre de sauts.
Comment obtenir la vraie adresse IP du client derrière un proxy?
Lisez-la depuis X-Forwarded-For par le réglage de proxy de confiance de votre cadriciel (req.ip dans Express avec trust proxy, ProxyFix(x_for=1) dans Flask, TrustProxies dans Laravel), pas en analysant l’en-tête vous-même.
Le HTTPS réglé en amont, un seul paramètre dans votre appli.
Canner émet les certificats, redirige le HTTP et envoie les en-têtes transmis. Vous ajoutez la ligne qu’il faut à votre cadriciel.