Adding rewrite rule dynamically

The issue with your updated code is that because you are using add_rewrite_rule() inside the filter, it can’t properly add it to the top of the list. You can see this if you dump out the $rules array. Because it gets added to the end of the $rules list, WP steps in beforehand to match on slug alone using some magic in the background.

In order to circumvent that process, you have to add it to the top of the list manually and return that from the filter.

Specifically, you need to change the add_rewrite_rule() call to this:

$rules = [$new_src => $new_dest] + $rules;

I included the full updated code below.

add_filter('rewrite_rules_array', function($rules) {

    $slug = 'foo';

    // get the request URI
    $uri = $_SERVER['REQUEST_URI'];
    
    // determine whether URI has slug in the first position
    if(preg_match('/^(\/' . $slug . ')/', $uri)){

        // get the base URI by removing the slug
        $base_uri = str_replace("https://wordpress.stackexchange.com/".$slug,'',$uri);

        // loop through existing rewrite rules
        foreach($rules as $src => $dest){

            // find the rewrite rule for which the base URI would have matched
            $regex_to_match="https://wordpress.stackexchange.com/" . str_replace("https://wordpress.stackexchange.com/",'\/',$src) . "https://wordpress.stackexchange.com/";
            
            preg_match_all($regex_to_match, $base_uri, $matches, PREG_SET_ORDER);


            if(count($matches) > 0){

                // get the specific matching groups
                $matches = $matches[0]; 

                // compile valid regex from URI with slug to create new rewrite source
                $new_src = ($uri[0] == "https://wordpress.stackexchange.com/" ? substr($uri, 1) : $uri) . '?$';
                
                // replace match variables with their string values to create new rewrite destination
                for($i=1; $i<count($matches)+1; $i++){
                    $replacement = isset($matches[$i]) ? $matches[$i] : '';
                    $replacement = strpos($replacement, "https://wordpress.stackexchange.com/") === 0 ? substr($replacement, 1) : $replacement;
                    $dest = str_replace('$matches[' . $i . ']', $replacement, $dest);
                }
                $new_dest = $dest;

                // add new rewrite rule to $wp_rewrite rules array
                $rules = [$new_src => $new_dest] + $rules;

                break;
            }
        }
    }

    

    return $rules;

});

This still feels like a weird way of doing this, and I’m sure there is a better one but without better understanding your use case I can’t really provide an alternate suggestion. The biggest issue here to me is you are flushing the rewrite rules on every request, which can be an expensive operation.

Original Answer

I wouldn’t hook into generate_rewrite_rules for this. You can just use the regular rewrite rules API.

Tested the below, and it works for redirecting http://example.com/foo/test/123 to https://example.com/test/123. Can customize it to your needs. Drop it in functions.php or a functionality plugin.

add_action( 'init',  function() {
    add_rewrite_rule( 'foo/([a-z0-9-/]+)[/]?$', 'index.php?foo_redirect_to=$matches[1]', 'top' );
} );

add_filter( 'query_vars', function( $query_vars ) {
    $query_vars[] = 'foo_redirect_to';
    return $query_vars;
} );

add_action('template_redirect', function() {
    if ( ( $redirect_to = get_query_var( 'foo_redirect_to' ) ) == false ) {
        return;
    }

    wp_redirect("https://example.com/$redirect_to");
});

error code: 523