HTTP3 and S2member content protection causing 302

Basically the title says it all - If I enable http3/quic on my server - then any kind of s2member protected content causes 302 too many redirects loop.

It don’t think that my s2member nginx content protection is causing it - but s2member must do something wrong.
This also happens when using worker_processes=1 without reuseport and when setting

fastcgi_param HTTP_HOST $host;
as described as common fix for wordpress here: https://pieterbakker.com/nginx-http3-missing-http-host-php-fpm-fix/

Note you have to use incognito mode in any browser or fully delete anything related to that page else you get served http 1.1 or h2 instead of h3 after enabling http3 on your server.

This is quite important for perfromance reasons as http3 is much faster than http2 and now used by about 50% of websites (yeah a lot behind cloudflared proxy / cache as cloudlflare went full http3/quic over a year ago or so.

That’s interesting… I’m not familiar with that setup, so I checked with chatgpt and got this back:

Short Version

It’s (almost certainly) not s2Member itself. It’s an HTTP/3 + Nginx → PHP-FPM issue where some server variables aren’t passed correctly. This makes WordPress mis-detect the canonical URL and/or HTTPS status, causing a 301/302 redirect loop—and since s2Member’s protected content triggers redirects sooner, you see it there first.


What’s Actually Going Wrong

When you enable HTTP/3 (QUIC), Nginx can handle the :authority and scheme differently. On some setups it won’t pass the right values for:

  • HTTP_HOST
  • SERVER_NAME
  • SERVER_PORT
  • HTTPS

to PHP-FPM unless you explicitly set them.

If those are wrong or missing, WordPress:

  • Builds canonical URLs with the wrong host/scheme → endless redirect loop.
  • Thinks the request is http (not https) → forces an SSL redirect forever.
  • Sets/reads cookies for the wrong domain → s2Member thinks you’re logged out.

The Fix (Do All of These)

In your PHP location/block that includes fastcgi_params, make these explicit:

# inside location ~ \.php$ { ... } or your php include
include fastcgi_params;

# The critical ones for HTTP/3 + PHP-FPM + WordPress
fastcgi_param HTTP_HOST       $host;                  # or $host:$server_port for non-standard ports
fastcgi_param SERVER_NAME     $host;
fastcgi_param SERVER_PORT     $server_port;
fastcgi_param REQUEST_SCHEME  $scheme;

# Ensure WP knows it’s HTTPS
fastcgi_param HTTPS           on;                     # or: fastcgi_param HTTPS $https;

# (optional, but helps in some stacks)
fastcgi_param HTTP_X_FORWARDED_PROTO $scheme;

Notes:

  • For non-standard ports, use:
    fastcgi_param HTTP_HOST $host:$server_port;
    
  • You still need a normal TLS listener alongside HTTP/3 (listen 443 ssl http2; listen 443 quic;).
  • The “incognito mode” reproduction is only because browsers prefer H3 when cache/cookies are fresh.

How to Verify Quickly

  1. Create phpinfo.php and load it over HTTP/3:

    curl --http3 -I https://example.com/phpinfo.php
    

    Check that:

    • $_SERVER['HTTP_HOST'] = your domain (and port if needed)
    • $_SERVER['HTTPS'] = on
    • $_SERVER['SERVER_PORT'] = 443
  2. Check Nginx logs for a host/scheme “ping-pong” in redirect chains (http :left_right_arrow: https, www :left_right_arrow: non-www).


Why s2Member Shows It First

Protected content triggers redirects earlier—s2Member defers to WP’s auth and canonical checks, so if WP is confused about the host or scheme, it loops instantly on protected URLs.


If It Still Loops After Fix

  • Force server vars in WP (for testing):

    add_action('muplugins_loaded', function () {
        $_SERVER['HTTPS'] = 'on';
        $_SERVER['SERVER_PORT'] = '443';
        if (empty($_SERVER['HTTP_HOST'])) {
            $_SERVER['HTTP_HOST'] = parse_url(get_option('home'), PHP_URL_HOST);
        }
    });
    
  • Disable canonical redirects temporarily:

    add_filter('redirect_canonical', '__return_false');
    

    If loop disappears, the cause is definitely server vars.

  • Check cookies: Ensure domain matches and Secure is set.


Unorthodox but Pragmatic Options

  • Terminate HTTP/3 at a proxy like Cloudflare/Caddy and let Nginx/PHP run over HTTP/2.
  • Remove Alt-Svc: h3=":443" header to keep browsers on H2 until fixed.
  • Hard-override HTTPS detection in wp-config.php:
    $_SERVER['HTTPS'] = 'on';
    

Bottom Line

Explicitly set your FastCGI params for HTTP/3:

fastcgi_param HTTP_HOST      $host;
fastcgi_param SERVER_NAME    $host;
fastcgi_param SERVER_PORT    443;      # or $server_port if needed
fastcgi_param REQUEST_SCHEME $scheme;
fastcgi_param HTTPS          on;

Once WordPress sees the correct host and HTTPS status, the s2Member 302 loop disappears.


Does that help?

:slight_smile:

You can try this in a must-use plugin, e.g. /wp-content/mu-plugins/fix-http3-redirs.php

<?php

add_action('muplugins_loaded', function () {
    // No-op if already good.
    if ((isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on') && !empty($_SERVER['HTTP_HOST'])) {
        return;
    }

    // Respect common proxy hints first
    if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
        $_SERVER['HTTPS'] = 'on';
        $_SERVER['SERVER_PORT'] = $_SERVER['SERVER_PORT'] ?? '443';
    } elseif (!empty($_SERVER['SERVER_PROTOCOL']) && stripos($_SERVER['SERVER_PROTOCOL'], 'HTTP/3') === 0) {
        // H3 is TLS-only; ensure HTTPS flag/port
        $_SERVER['HTTPS'] = $_SERVER['HTTPS'] ?? 'on';
        $_SERVER['SERVER_PORT'] = $_SERVER['SERVER_PORT'] ?? '443';
    }

    // Fill missing host from WP settings as a last resort
    if (empty($_SERVER['HTTP_HOST'])) {
        $home_host = parse_url(get_option('home'), PHP_URL_HOST);
        if ($home_host) {
            $_SERVER['HTTP_HOST'] = $home_host;
        }
    }
}, 0);

:slight_smile:

Edit - it’s embarrasing to say, but I had at some point removed
include fastcgi_params;

from my nginx.conf file. Funny enough that besides s2member and a single other plugin - qtranslate-xt all other wordpress plugins ran well with it - and it seemingly was okay with http2 / http1.1 as well.

Some places mention that it’s supposed to work without those params and wordpress got corrected to run witthout them too. But yeah guess it’s a lot of code changing in any plugin to make it work without.

Me then adding stuff to that file then didn’t change anything of course. So good news at least with the following content of fastcgi_params file and that being called correctly in nginx.conf all is fine:

fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
fastcgi_param  REQUEST_URI        $request_uri;
fastcgi_param  DOCUMENT_URI       $document_uri;
fastcgi_param  DOCUMENT_ROOT      $document_root;
fastcgi_param  SERVER_PROTOCOL    $server_protocol;
fastcgi_param  REQUEST_SCHEME     $scheme;
fastcgi_param  HTTPS              $https if_not_empty;

fastcgi_param  GATEWAY_INTERFACE  CGI/1.1;
fastcgi_param  SERVER_SOFTWARE    nginx/$nginx_version;

fastcgi_param  REMOTE_ADDR        $remote_addr;
fastcgi_param  REMOTE_PORT        $remote_port;
fastcgi_param  SERVER_ADDR        $server_addr;
fastcgi_param  SERVER_PORT        $server_port;
fastcgi_param  SERVER_NAME 		  $host;
fastcgi_param  HTTPS on;

# PHP only, required if PHP was built with --enable-force-cgi-redirect
fastcgi_param  REDIRECT_STATUS    200;

# Fix for http3: https://pieterbakker.com/nginx-http3-missing-http-host-php-fpm-fix/
fastcgi_param HTTP_HOST $host;
1 Like

That’s great news! Thank you for the update.

:slight_smile: