How to Fix 421 Misdirected Request Error in PHP/Apache When Using Multi-Domain SSL Certificates


1 views

For years, I struggled with a persistent yet intermittent 421 Misdirected Request error that appeared when:

  • Using PHP header() redirects between domains
  • Loading cross-domain resources (JS/CSS/images)
  • Particularly in Safari browsers

The core issue stems from Server Name Indication (SNI) mismatches when:

1. Client requests DomainA.com
2. Server establishes TLS connection with DomainB.com's SNI
3. Browser detects SNI/Domain mismatch
4. Apache terminates connection with 421

Common problematic patterns in PHP code:

// Redirect triggering SNI mismatch
header("Location: https://static.example.com/resource");
exit;

// Resource loading vulnerable to 421
<script src="https://cdn.example.com/jquery.js"></script>

Solution 1: Separate SSL Certificates (Recommended)

# .htaccess configuration for domain-specific certs
<IfModule mod_ssl.c>
    <VirtualHost *:443>
        SSLCertificateFile /path/to/domain1.crt
        SSLCertificateKeyFile /path/to/domain1.key
        ServerName domain1.com
    </VirtualHost>
    
    <VirtualHost *:443>
        SSLCertificateFile /path/to/domain2.crt
        SSLCertificateKeyFile /path/to/domain2.key
        ServerName domain2.com
    </VirtualHost>
</IfModule>

Solution 2: HTTP/2 Connection Reuse Optimization

# Apache configuration to force proper SNI
SSLStrictSNIVHostCheck on
SSLUseStapling on

For Safari compatibility, implement these PHP checks:

function isSafariSNISensitive() {
    $ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
    if (preg_match('/Version\/[0-9.]+.*Safari/', $ua)) {
        return true;
    }
    return false;
}

if (isSafariSNISensitive()) {
    // Serve resources from primary domain
    $resourceBase = 'https://main-domain.com/static/';
} else {
    $resourceBase = 'https://cdn-domain.com/';
}

Essential troubleshooting commands:

# Check active SNI configuration
openssl s_client -connect example.com:443 -servername example.com -tlsextdebug

# Verify certificate chain
curl -v https://example.com --resolve example.com:443:IP_ADDRESS

After wrestling with intermittent 421 "Misdirected Request" errors across my PHP applications for nearly two years, I finally cracked the case. This HTTP status indicates a TLS/SNI mismatch where the client (usually Safari) expects a different hostname than what the server presents during the SSL handshake.

The core issue stems from how browsers handle connection reuse with multi-domain certificates. When your Apache server serves:


// PHP redirect from domainA.com to domainB.com
header("Location: https://domainB.com/resource");

The browser might attempt to reuse the existing TLS connection, but the SNI (Server Name Indication) from the initial domainA.com handshake conflicts with domainB.com's requirements.

Modern browsers implement connection coalescing - attempting to reuse existing connections when:

  • IP addresses match
  • Certificate covers both hostnames
  • Server supports HTTP/2

This optimization backfires when:


// Problematic flow:
1. User visits https://static.example.com/image.jpg (SNI: static.example.com)
2. App redirects to https://app.example.com/dashboard (SNI mismatch)
3. Browser reuses connection → 421 error

Option 1: Separate Certificates

The most reliable fix is using dedicated certificates per domain:


# Apache VirtualHost configuration
<VirtualHost *:443>
    ServerName app.example.com
    SSLCertificateFile /path/to/app_cert.pem
    SSLCertificateKeyFile /path/to/app_key.pem
</VirtualHost>

<VirtualHost *:443>
    ServerName static.example.com
    SSLCertificateFile /path/to/static_cert.pem
    SSLCertificateKeyFile /path/to/static_key.pem
</VirtualHost>

Option 2: HTTP/2 Optimization

If separate certificates aren't feasible, tweak HTTP/2 settings:


# In .htaccess or Apache config
Protocols h2 http/1.1
H2Direct off

When handling redirects between domains:


// Add connection-closing headers before redirect
header("Connection: close");
header("Location: https://newdomain.com/page");
exit();

Safari requires special handling due to aggressive connection reuse:


// Detect Safari and force full reload
if (strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') && 
    !strpos($_SERVER['HTTP_USER_AGENT'], 'Chrome')) {
    header("Cache-Control: no-store");
}

Use this curl command to test SNI handling:


curl -v -H "Host: domainB.com" --resolve domainB.com:443:IP_ADDRESS \
https://domainB.com --connect-to ::domainA.com

Key indicators of success:

  • No "421 Misdirected Request" in output
  • Correct certificate shown in verbose output
  • Server uses proper SNI hostname