Rewrite rule to load images from production does nothing

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . index.php [L]

# If images not found on development site, load from production 
RewriteCond %{REQUEST_URI} ^/wp-content/uploads/[^\/]*/.*$
RewriteRule ^(.*)$ https://www.example.com/$1 [QSA,L]

The problem here is that “If images not found on development site” then the request has already been rewritten to index.php by the preceding RewriteRule (WordPress front-controller), so your rule block that follows does nothing.

NB: Whilst you’ve not explicitly included the R (redirect) flag on the RewriteRule, this will implicitly trigger an external 302 (temporary) redirect, the same as if you had explicitly included R=302 on the directive. Explicitly including this flag is preferable to more clearly communicate its intent. In fact, you might want to change this to a 301 so that images are cached, thus preventing external redirects to your production server on every request.

The QSA flag is not required here, since you are not including a query string on the RewriteRule susbstitution.

Aside: Your RewriteRule arguably matches too match… it matches everything, not just images, is that intentional?

You could resolve this by either…

  1. preventing all image URLs being processed by the front-controller. For example, add an additional condition to the WP front-controller to exclude images:

    RewriteCond %{REQUEST_URI} !\.(jpe?g|png|gif)$
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . index.php [L]
    
  2. OR, move your redirect to above the WP front-controller, and check for the non-existence of the requested file before redirecting to your production server. I would also be more restrictive on the regex and match only images (as mentioned above) – if that is the intention. For example:

    RewriteRule ^index\.php$ - [L]
    
    # If images not found on development site, load from production
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^wp-content/uploads/[^/]+/.+\.(jpe?g|png|gif)$ https://www.example.com/$0 [R=302,L]
    
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . index.php [L]
    

The additional condition is not required, as you can perform the URL comparison in the RewriteRule pattern (more efficient). The slash does not need to be escaped in the character class (or anywhere for that matter). And the QSA flag is not required here (as mentioned above).

Note that the regex matches URL-paths of the form /wp-content/uploads/<somedirectory>/<something>.jpg, where <something> can be any number of additional subdirectories. This is based on your regex.

The $0 backreference (as opposed to $1 in your original directive) is the entire URL-path that is matched by the RewriteRule pattern. $1 contains the first captured subgroup. In the revised directive, the first captured subgroup contains just the image file extension.