Why Drupal Serving HTTP Even After Enabling HTTPS on the Server
Introduction
When you switch your Drupal site to HTTPS whether via Apache, NGINX you expect Drupal to recognise it’s running securely. But then you notice something off:
- Internal links are still rendered as http://.
- $request->isSecure() returns false.
- Redirects start looping or downgrading to HTTP.
- You see Mixed Content warnings.
On the browser side, it looks secure. But Drupal doesn’t agree. Let’s break down why, what it causes, and how to fix it.
Why This Happens
Drupal determines whether a request is secure by inspecting $_SERVER variables like:
- $_SERVER[‘HTTPS’]
- $_SERVER[‘HTTP_X_FORWARDED_PROTO’]
But if you’re 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.
If the headers aren’t forwarded or handled correctly, Drupal continues to think the request is insecure.
There are the following ways to overcome such issues.
Option 1: Update settings.php
Before you write custom code, Drupal actually supports reverse proxy handling natively if you tell it to.
Add the following lines to your sites/default/settings.php file
$settings['reverse_proxy'] = TRUE; $settings['reverse_proxy_addresses'] = [$_SERVER['REMOTE_ADDR']];
Here’s what this does:
- reverse_proxy = TRUE tells Drupal to trust forwarded headers from proxies.
- reverse_proxy_addresses defines which IPs Drupal should trust as reverse proxies (use a static IP or CIDR range in production)
Once this is in place, Drupal will look at headers like X-Forwarded-Proto and properly detect HTTPS.
But that only works if your proxy is sending the correct headers and Drupal is trusting them.
Option 2: Custom Middleware to Enforce HTTPS Recognition
If you’re in a more complex environment (e.g. multiple proxies, dynamic IPs, or inconsistent headers), or if the settings.php solution isn’t reliable, here’s another approach:
You can inject a middleware that explicitly tells Drupal “this request is secure” based on your own conditions (like environment or headers). This gives you full control. I have created a custom module my_module in the custom module folder.
What This Middleware Does
The purpose of the middleware in our example is straightforward:
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.
Forward the updated request for normal Drupal handling.
The Implementation
Step 1: Create a Middleware Class
File: modules/custom/my_module/src/StackMiddleware/MyModuleMiddleware.php
<?php namespace Drupal\my_module\StackMiddleware; use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Drupal\Core\Site\Settings; /** * MyModuleMiddleware middleware. */ class MyModuleMiddleware implements HttpKernelInterface { use StringTranslationTrait; /** * The kernel. * * @var \Symfony\Component\HttpKernel\HttpKernelInterface */ protected $httpKernel; /** * Constructs the MyModuleMiddleware object. * * @param \Symfony\Component\HttpKernel\HttpKernelInterface $http_kernel * The decorated kernel. */ public function __construct(HttpKernelInterface $http_kernel) { $this->httpKernel = $http_kernel; } /** * {@inheritdoc} */ public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true) { // Enforce HTTPS in production. if (Settings::get('env_name') == 'prod') { $request->headers->set('x-forwarded-proto', 'https'); $request->server->set('HTTPS', 'HTTPS'); } return $this->httpKernel->handle($request, $type, $catch); } }
Key Points Explained
- Namespace & Imports
The class is placed under the module’s StackMiddleware namespace and imports essential interfaces and classes from both Symfony and Drupal. - Implements HttpKernelInterface
By implementing the Symfony kernel interface, this class integrates seamlessly into the Drupal request lifecycle. - Decorated Kernel Pattern
The 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. - Environment Awareness
The environment is determined using Settings::get(‘env_name’). This makes the enforcement conditional, ensuring that developers in local or staging environments are not forced into HTTPS unless necessary. - Enforcing HTTPS
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.
2. The $_SERVER[‘HTTPS’] variable is explicitly set to HTTPS, ensuring Symfony and Drupal both treat the connection as secure.
Step 2: Register the Middleware as a Service
File: modules/custom/my_module/my_module.services.yml
services: my_module.middleware: class: Drupal\my_module\StackMiddleware\MyModuleMiddleware tags: - { name: http_middleware, priority: 1000 }
How to Ensure and Test the Middleware
Once you have implemented and registered your middleware, it’s important to validate that it behaves as expected. Here are a few steps you can follow to verify its enforcement of HTTPS in production:
1. Confirm the Environment Setting
Ensure your settings.php (or settings.prod.php if you split configs) contains the environment name:
$settings['env_name'] = 'prod';
This setting is critical because the middleware only applies its logic when env_name is set to prod.
2. Simulate a Non‑HTTPS Request
From your local terminal or using a tool like cURL, simulate an HTTP request:
curl -I http://yoursite.com
- Without the middleware, Drupal may treat this as plain HTTP.
- With the middleware active in production, the response headers (or subsequent redirect rules) should indicate an HTTPS context.
3. Check Server Variables Inside Drupal
Add temporary debugging or use \Drupal::request()->server inspector to check if HTTPS is being set properly:
\Drupal::logger('debug')->notice('HTTPS value: @https', ['@https' => $_SERVER['HTTPS'] ?? 'not set']);
In production, this should log HTTPS value: HTTPS.
5. Browser-Level Validation
- Visit your Drupal site in production via http://.
- You should automatically be redirected to or treated as https://.
- All secure routes (login, forms, etc.) should now behave without mixed-content warnings or insecure cookie issues.
Conclusion
If Drupal isn’t recognizing HTTPS, even when your browser shows it’s secure, it’s likely due to how PHP sees the incoming request, especially behind proxies or load balancers.
There are two solid ways to fix this:
- Quick fix: Use $settings[‘reverse_proxy’] = TRUE in settings.php. This is the default and most Drupal-friendly approach.
- Controlled fix: Write a middleware that forcefully sets the correct flags based on headers or environment.
The middleware gives you more flexibility and is great for edge cases. But for most people, updating settings.php is enough.
Either way, getting this right prevents redirect loops, mixed content warnings, and misbehaving $request->isSecure() checks.
Hope this saves someone the hours I spent chasing phantom HTTP bugs in an HTTPS world.
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.