Dragon. The PDF itself is fine—your proxy isn’t returning a stream the browser can render. Here’s a drop-in proxy route that works + a 2-step checklist to make sure the request actually reaches your server from the client.

1) Server route (paste as-is)

This allows GCS and Firebase Storage hosts, forwards Range, preserves key headers, and streams the body without buffering.

// /api/preview/pdf?u=<encoded GCS/Firebase URL>&filename=optional.pdf
app.get('/api/preview/pdf', async (req, res) => {
  try {
    const u = req.query.u;
    if (!u) return res.status(400).send('Missing ?u');

    const url = decodeURIComponent(String(u));
    // Allow both GCS + Firebase Storage (app domains)
    const allowed = /^(https:\/\/(?:storage\.googleapis\.com|firebasestorage\.googleapis\.com|[^/]+\.firebasestorage\.app)\/)/i;
    if (!allowed.test(url)) return res.status(400).send('Blocked');

    const headers = req.headers.range ? { Range: req.headers.range } : {};
    const upstream = await fetch(url, { headers });

    // Mirror status + important headers used by the built-in PDF viewer
    res.status(upstream.status);
    for (const h of [
      'content-length', 'content-range', 'accept-ranges',
      'cache-control', 'etag', 'last-modified',
      'content-disposition', 'content-type'
    ]) {
      const v = upstream.headers.get(h);
      if (v) res.setHeader(h, v);
    }
    // Ensure sane defaults if upstream doesn’t set them
    if (!res.getHeader('content-type')) res.setHeader('Content-Type', 'application/pdf');
    if (!res.getHeader('content-disposition')) {
      const name = (req.query.filename || 'document.pdf').toString();
      res.setHeader('Content-Disposition', `inline; filename="${name}"`);
    }

    if (!upstream.body) return res.end();
    const { Readable } = await import('node:stream');
    Readable.fromWeb(upstream.body).pipe(res);
  } catch (err) {
    console.error('PDF proxy error:', err);
    res.status(502).send('Proxy error');
  }
});

2) Frontend: build the URL correctly

Be sure you’re pointing the modal <iframe>/<object> at your proxy and the Firebase URL is URL-encoded.

const direct = template.pdfUrl; // your signed GCS/Firebase URL
const proxied = `/api/preview/pdf?u=${encodeURIComponent(direct)}&filename=${encodeURIComponent(template.title || 'template.pdf')}`;


Use proxied for the viewer’s src/data.
(Your message showed the same long GCS link twice—make sure the viewer isn’t still using the direct URL by mistake.)

3) Make sure the client can reach /api

If you’re on Vite/CRA with a separate Express server, add a dev proxy so the browser request hits your Node app:

vite.config.ts

export default defineConfig({
  server: {
    proxy: {
      '/api': 'http://localhost:3000' // or your server port
    }
  }
});

4) Quick verification (copy/paste)

From a shell where your server is reachable:

# Replace <ENCODED_URL> with encodeURIComponent(your Firebase URL)
curl -I "http://localhost:3000/api/preview/pdf?u=<ENCODED_URL>"
# Expect: 200 OK (or 206 Partial Content) and headers:
#   content-type: application/pdf
#   accept-ranges: bytes
#   content-disposition: inline; ...


If that returns the right headers, your modal will render the PDF instead of a blank pane.