fix(basemap-tile): buffer body + drop upstream encoding/length headers

Initial proxy streamed upstream.body straight through with the upstream
Content-Encoding + Content-Length headers. Two ways that broke:

  - Node's fetch auto-decodes gzip/br responses, so the body coming
    out of upstream.body is already plain bytes. Forwarding
    Content-Encoding: gzip made the browser (and curl) try to gunzip
    plain bytes and fail.
  - Content-Length was the upstream (compressed) length, not the
    decoded byte count. Mid-stream the H2 layer noticed the mismatch
    and dropped with INTERNAL_ERROR (curl returned status=000 + a
    0-byte file).

Switch to arrayBuffer() + emit only Content-Type. Node serializes
the response with the right length and no encoding header, so the
browser gets the plain PBF / PNG / JSON it expects.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-05-24 10:47:18 +03:00
parent efcfa66c07
commit 44ba50f226
+9 -4
View File
@@ -70,12 +70,17 @@ export async function GET(
return NextResponse.json({ error: "upstream_too_large" }, { status: 413 }); return NextResponse.json({ error: "upstream_too_large" }, { status: 413 });
} }
// Buffer the body and re-emit. We deliberately drop upstream
// Content-Encoding + Content-Length: Node's fetch auto-decodes
// gzip/br responses, so forwarding the original encoding header makes
// the browser try to gunzip already-decoded bytes (or, worse, makes
// Next.js stream a Content-Length that doesn't match the decoded
// payload → HTTP/2 INTERNAL_ERROR mid-stream).
const body = await upstream.arrayBuffer();
const headers = new Headers(); const headers = new Headers();
const ct = upstream.headers.get("content-type"); const ct = upstream.headers.get("content-type");
if (ct) headers.set("Content-Type", ct); if (ct) headers.set("Content-Type", ct);
const ce = upstream.headers.get("content-encoding");
if (ce) headers.set("Content-Encoding", ce);
if (contentLength) headers.set("Content-Length", contentLength);
// Tiles + sprites + glyphs are immutable per path (versioned). Cache // Tiles + sprites + glyphs are immutable per path (versioned). Cache
// aggressively to keep architots out of the per-tile critical path. // aggressively to keep architots out of the per-tile critical path.
@@ -84,5 +89,5 @@ export async function GET(
"public, max-age=86400, stale-while-revalidate=604800, immutable", "public, max-age=86400, stale-while-revalidate=604800, immutable",
); );
return new NextResponse(upstream.body, { status: 200, headers }); return new NextResponse(body, { status: 200, headers });
} }