From 44ba50f226a389de892430df2ee86939ba703efa Mon Sep 17 00:00:00 2001 From: Claude VM Date: Sun, 24 May 2026 10:47:18 +0300 Subject: [PATCH] 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) --- src/app/api/basemap-tile/[...path]/route.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/app/api/basemap-tile/[...path]/route.ts b/src/app/api/basemap-tile/[...path]/route.ts index 6009a13..bd5db19 100644 --- a/src/app/api/basemap-tile/[...path]/route.ts +++ b/src/app/api/basemap-tile/[...path]/route.ts @@ -70,12 +70,17 @@ export async function GET( 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 ct = upstream.headers.get("content-type"); 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 // 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", ); - return new NextResponse(upstream.body, { status: 200, headers }); + return new NextResponse(body, { status: 200, headers }); }