{"id":72325,"date":"2025-05-28T11:46:56","date_gmt":"2025-05-28T06:16:56","guid":{"rendered":"https:\/\/www.tothenew.com\/blog\/?p=72325"},"modified":"2025-08-29T16:05:54","modified_gmt":"2025-08-29T10:35:54","slug":"why-drupal-still-thinks-its-serving-http-even-after-enabling-https-on-the-server","status":"publish","type":"post","link":"https:\/\/www.tothenew.com\/blog\/why-drupal-still-thinks-its-serving-http-even-after-enabling-https-on-the-server\/","title":{"rendered":"Why Drupal Serving HTTP Even After Enabling HTTPS on the Server"},"content":{"rendered":"<h2>Introduction<\/h2>\n<p>When you switch your Drupal site to HTTPS whether via Apache, NGINX you expect Drupal to recognise it&#8217;s running securely. But then you notice something off:<\/p>\n<ul>\n<li>Internal links are still rendered as http:\/\/.<\/li>\n<li>$request-&gt;isSecure() returns false.<\/li>\n<li>Redirects start looping or downgrading to HTTP.<\/li>\n<li>You see Mixed Content warnings.<\/li>\n<\/ul>\n<p>On the browser side, it looks secure. But Drupal doesn\u2019t agree. Let\u2019s break down why, what it causes, and how to fix it.<\/p>\n<h2>Why This Happens<\/h2>\n<p>Drupal determines whether a request is secure by inspecting $_SERVER variables like:<\/p>\n<ul>\n<li>$_SERVER[&#8216;HTTPS&#8217;]<\/li>\n<li>$_SERVER[&#8216;HTTP_X_FORWARDED_PROTO&#8217;]<\/li>\n<\/ul>\n<p>But if you\u2019re behind a reverse proxy or load balancer, the HTTPS handshake might happen before the request even reaches your Drupal server. Meaning: your app receives a plain HTTP request and never sees that it was originally HTTPS.<\/p>\n<p>If the headers aren\u2019t forwarded or handled correctly, Drupal continues to think the request is insecure.<\/p>\n<p>There are the following ways to overcome such issues.<\/p>\n<h2>Option 1: Update settings.php<\/h2>\n<p>Before you write custom code, Drupal actually supports reverse proxy handling natively if you tell it to.<\/p>\n<p>Add the following lines to your sites\/default\/settings.php file<\/p>\n<pre>$settings['reverse_proxy'] = TRUE;\r\n$settings['reverse_proxy_addresses'] = [$_SERVER['REMOTE_ADDR']];<\/pre>\n<p>Here\u2019s what this does:<\/p>\n<ul>\n<li>reverse_proxy = TRUE tells Drupal to trust forwarded headers from proxies.<\/li>\n<li>reverse_proxy_addresses defines which IPs Drupal should trust as reverse proxies (use a static IP or CIDR range in production)<\/li>\n<\/ul>\n<p>Once this is in place, Drupal will look at headers like X-Forwarded-Proto and properly detect HTTPS.<\/p>\n<p>But that only works if your proxy is sending the correct headers and Drupal is trusting them.<\/p>\n<h2>Option 2: Custom Middleware to Enforce HTTPS Recognition<\/h2>\n<p>If you&#8217;re in a more complex environment (e.g. multiple proxies, dynamic IPs, or inconsistent headers), or if the settings.php solution isn&#8217;t reliable, here\u2019s another approach:<\/p>\n<p>You can inject a middleware that explicitly tells Drupal \u201cthis request is secure\u201d based on your own conditions (like environment or headers). This gives you full control. I have created a custom module <strong>my_module <\/strong>in the custom module folder.<\/p>\n<p><strong>What This Middleware Does<\/strong><br \/>\nThe purpose of the middleware in our example is straightforward:<\/p>\n<p>Detect whether the site is running in the production environment. If so, force the request to be treated as HTTPS, regardless of how it arrives.<br \/>\nForward the updated request for normal Drupal handling.<\/p>\n<h3>The Implementation<\/h3>\n<h3>Step 1: Create a Middleware Class<\/h3>\n<p>File: modules\/custom\/my_module\/src\/StackMiddleware\/MyModuleMiddleware.php<\/p>\n<pre>&lt;?php\r\n\r\nnamespace Drupal\\my_module\\StackMiddleware;\r\n\r\nuse Drupal\\Core\\StringTranslation\\StringTranslationTrait;\r\nuse Symfony\\Component\\HttpFoundation\\Request;\r\nuse Symfony\\Component\\HttpFoundation\\Response;\r\nuse Symfony\\Component\\HttpKernel\\HttpKernelInterface;\r\nuse Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException;\r\nuse Drupal\\Core\\Site\\Settings;\r\n\r\n\/**\r\n* MyModuleMiddleware middleware.\r\n*\/\r\nclass MyModuleMiddleware implements HttpKernelInterface\r\n{\r\n\r\nuse StringTranslationTrait;\r\n\r\n\/**\r\n* The kernel.\r\n*\r\n* @var \\Symfony\\Component\\HttpKernel\\HttpKernelInterface\r\n*\/\r\nprotected $httpKernel;\r\n\r\n\/**\r\n* Constructs the MyModuleMiddleware object.\r\n*\r\n* @param \\Symfony\\Component\\HttpKernel\\HttpKernelInterface $http_kernel\r\n* The decorated kernel.\r\n*\/\r\npublic function __construct(HttpKernelInterface $http_kernel)\r\n{\r\n  $this-&gt;httpKernel = $http_kernel;\r\n}\r\n\r\n\/**\r\n* {@inheritdoc}\r\n*\/\r\npublic function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)\r\n{\r\n  \/\/ Enforce HTTPS in production.\r\n  if (Settings::get('env_name') == 'prod') {\r\n    $request-&gt;headers-&gt;set('x-forwarded-proto', 'https');\r\n    $request-&gt;server-&gt;set('HTTPS', 'HTTPS');\r\n  }\r\n  return $this-&gt;httpKernel-&gt;handle($request, $type, $catch);\r\n}\r\n\r\n}\r\n\r\n\r\n<\/pre>\n<p><strong>Key Points Explained<\/strong><\/p>\n<ol>\n<li><strong>Namespace &amp; Imports<\/strong><br \/>\nThe class is placed under the module\u2019s\u00a0StackMiddleware\u00a0namespace and imports essential interfaces and classes from both Symfony and Drupal.<\/li>\n<li><strong>Implements HttpKernelInterface<\/strong><br \/>\nBy implementing the Symfony kernel interface, this class integrates seamlessly into the Drupal request lifecycle.<\/li>\n<li><strong>Decorated Kernel Pattern<\/strong><br \/>\nThe middleware stores a reference to the original Drupal kernel ($httpKernel). Once it completes its logic, it passes control back to the kernel, ensuring the application continues normally.<\/li>\n<li><strong>Environment Awareness<\/strong><br \/>\nThe environment is determined using\u00a0Settings::get(&#8216;env_name&#8217;). This makes the enforcement conditional, ensuring that developers in local or staging environments are not forced into HTTPS unless necessary.<\/li>\n<li><strong>Enforcing HTTPS<br \/>\n<\/strong>\u00a0 1. x-forwarded-proto is set to https. This header is vital when working behind a reverse proxy, signaling that the request originally came through HTTPS.<br \/>\n2. The $_SERVER[&#8216;HTTPS&#8217;] variable is explicitly set to HTTPS, ensuring Symfony and Drupal both treat the connection as secure.<\/li>\n<\/ol>\n<h3>Step 2: Register the Middleware as a Service<\/h3>\n<p>File: modules\/custom\/my_module\/my_module.services.yml<\/p>\n<p>&nbsp;<\/p>\n<pre>services: \r\nmy_module.middleware:\r\nclass: Drupal\\my_module\\StackMiddleware\\MyModuleMiddleware\r\ntags:\r\n- { name: http_middleware, priority: 1000 }<\/pre>\n<p>&nbsp;<\/p>\n<p><strong>How to Ensure and Test the Middleware<\/strong><br \/>\nOnce you have implemented and registered your middleware, it\u2019s important to validate that it behaves as expected. Here are a few steps you can follow to verify its enforcement of HTTPS in production:<\/p>\n<p>1.\u00a0Confirm the Environment Setting<br \/>\nEnsure your\u00a0settings.php\u00a0(or\u00a0settings.prod.php\u00a0if you split configs) contains the environment name:<\/p>\n<pre>$settings['env_name'] = 'prod';<\/pre>\n<p>This setting is critical because the middleware only applies its logic when\u00a0env_name\u00a0is set to\u00a0prod.<\/p>\n<p>2.\u00a0Simulate a Non\u2011HTTPS Request<br \/>\nFrom your local terminal or using a tool like\u00a0cURL, simulate an HTTP request:<\/p>\n<pre>curl -I http:\/\/yoursite.com<\/pre>\n<ul>\n<li>Without the middleware, Drupal may treat this as plain HTTP.<\/li>\n<li>With the middleware active in production, the response headers (or subsequent redirect rules) should indicate an HTTPS context.<\/li>\n<\/ul>\n<p>3.\u00a0Check Server Variables Inside Drupal<br \/>\nAdd temporary debugging or use\u00a0\\Drupal::request()-&gt;server\u00a0inspector to check if\u00a0HTTPS\u00a0is being set properly:<\/p>\n<pre>\\Drupal::logger('debug')-&gt;notice('HTTPS value: @https', ['@https' =&gt; $_SERVER['HTTPS'] ?? 'not set']);<\/pre>\n<p>In production, this should log\u00a0HTTPS value: HTTPS.<\/p>\n<p>5.\u00a0Browser-Level Validation<\/p>\n<ul>\n<li>Visit your Drupal site in production via http:\/\/.<\/li>\n<li>You should automatically be redirected to or treated as https:\/\/.<\/li>\n<li>All secure routes (login, forms, etc.) should now behave without mixed-content warnings or insecure cookie issues.<\/li>\n<\/ul>\n<h2>Conclusion<\/h2>\n<p>If Drupal isn\u2019t recognizing HTTPS, even when your browser shows it\u2019s secure, it\u2019s likely due to how PHP sees the incoming request, especially behind proxies or load balancers.<\/p>\n<p>There are two solid ways to fix this:<\/p>\n<ol>\n<li>Quick fix: Use $settings[&#8216;reverse_proxy&#8217;] = TRUE in settings.php. This is the default and most Drupal-friendly approach.<\/li>\n<li>Controlled fix: Write a middleware that forcefully sets the correct flags based on headers or environment.<\/li>\n<\/ol>\n<p>The middleware gives you more flexibility and is great for edge cases. But for most people, updating settings.php is enough.<\/p>\n<p>Either way, getting this right prevents redirect loops, mixed content warnings, and misbehaving $request-&gt;isSecure() checks.<\/p>\n<p>Hope this saves someone the hours I spent chasing phantom HTTP bugs in an HTTPS world.<\/p>\n<p>Please let us know if you encounter any issues during the integration process, and feel free to reach out in the comments if you have any questions.<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction When you switch your Drupal site to HTTPS whether via Apache, NGINX you expect Drupal to recognise it&#8217;s running securely. But then you notice something off: Internal links are still rendered as http:\/\/. $request-&gt;isSecure() returns false. Redirects start looping or downgrading to HTTP. You see Mixed Content warnings. On the browser side, it looks [&hellip;]<\/p>\n","protected":false},"author":1501,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"iawp_total_views":51},"categories":[3602],"tags":[4862,1157,1220,1192],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/72325"}],"collection":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/users\/1501"}],"replies":[{"embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/comments?post=72325"}],"version-history":[{"count":11,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/72325\/revisions"}],"predecessor-version":[{"id":74458,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/posts\/72325\/revisions\/74458"}],"wp:attachment":[{"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/media?parent=72325"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/categories?post=72325"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.tothenew.com\/blog\/wp-json\/wp\/v2\/tags?post=72325"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}