Repository navigation
fix(plugin): gunzip .xml.gz sitemaps served as gzip files; v0.106.1 - #256
jjohnson-hdb wants to merge 1 commit into
Conversation
A sitemap child served as a gzip FILE (`application/x-gzip`, no Content-Encoding) — the sitemaps.org convention for large sitemaps — reached `parseSitemap` still compressed, because undici decodes only a Content-Encoding. Every such child failed with "non-sitemap response ... Maximum nested tags exceeded", so an index of `.xml.gz` children created no targets at all. `decodeSitemapBody` (util/sitemap.js) now reads the raw bytes, gunzips them when they start with the gzip magic (`1f 8b`) — async, capped at 100 MB decompressed — and otherwise decodes exactly as `Response.text()` did (UTF-8, BOM stripped). A truncated or oversize gzip file fails the child loudly. Transport-encoded bodies are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces support for gzipped sitemaps (.xml.gz) served as raw gzip files without a Content-Encoding header. It implements a decodeSitemapBody utility that sniffs for gzip magic bytes, decompresses the body asynchronously up to a 100 MB limit to prevent gzip bombs, and falls back to plain text decoding. Comprehensive tests are added to cover various decompression scenarios. The feedback recommends improving error handling in the catch blocks of Sitemap.js and sitemap.js by safely accessing the error message using e?.message ?? String(e) to handle non-standard exceptions robustly.
| } catch (e) { | ||
| throw new Error(`Sitemap fetch for ${url}${via} (status ${res.status}): ${e.message}`); | ||
| } |
There was a problem hiding this comment.
When catching exceptions, it is safer to handle non-standard errors or string throws by using e?.message ?? String(e) instead of directly accessing e.message. If a non-standard error or string is thrown, e.message will evaluate to undefined, resulting in an unhelpful error message.
| } catch (e) { | |
| throw new Error(`Sitemap fetch for ${url}${via} (status ${res.status}): ${e.message}`); | |
| } | |
| } catch (e) { | |
| throw new Error('Sitemap fetch for ' + url + via + ' (status ' + res.status + '): ' + (e?.message ?? String(e))); | |
| } |
References
- When handling caught exceptions, safely access the error message using optional chaining and fall back to String(e) (e.g., e?.message ?? String(e)) to ensure that the error handling or logging block itself does not throw a TypeError if the caught exception is null, undefined, or a non-object.
| } catch (e) { | ||
| throw new Error(`gzip sitemap body could not be decompressed (${bytes.length} bytes): ${e.message}`); | ||
| } |
There was a problem hiding this comment.
When catching exceptions, it is safer to handle non-standard errors or string throws by using e?.message ?? String(e) instead of directly accessing e.message. If a non-standard error or string is thrown, e.message will evaluate to undefined, resulting in an unhelpful error message.
| } catch (e) { | |
| throw new Error(`gzip sitemap body could not be decompressed (${bytes.length} bytes): ${e.message}`); | |
| } | |
| } catch (e) { | |
| throw new Error('gzip sitemap body could not be decompressed (' + bytes.length + ' bytes): ' + (e?.message ?? String(e))); | |
| } |
References
- When handling caught exceptions, safely access the error message using optional chaining and fall back to String(e) (e.g., e?.message ?? String(e)) to ensure that the error handling or logging block itself does not throw a TypeError if the caught exception is null, undefined, or a non-object.
Problem
.xml.gzis the sitemaps.org convention for large sitemaps, and most big catalogs publish the children of their index that way. An origin serves such a child as a gzip file:content-type: application/x-gzip, noContent-Encoding. undici decodes only a Content-Encoding, sofetchSitemapread the compressed bytes withres.text()and handed them to the XML parser. Every such child failed:An index made only of
.xml.gzchildren therefore created zero targets. A weekly or daily refresh would fail the same way on every run. Nothing in the plugin decompressed a gzip file: zlib was used only for page storage, the change probe and sampling.Fix
decodeSitemapBody(bytes, { maxBytes })inutil/sitemap.js:1f 8b: asyncgunzip, capped atMAX_SITEMAP_DECOMPRESSED_BYTES(100 MB, twice the sitemaps.org 50 MB limit).TextDecoder, which is exactly whatResponse.text()did (UTF-8, BOM stripped)..gzsuffix, because origins label these files inconsistently and no XML document starts with1f 8b.fetchSitemapreadsarrayBuffer()and decodes through the helper. A truncated, corrupt or oversize gzip file throws, so the child fails loudly rather than being read as a shorter sitemap. That matters for departures: see the existing truncated-gzip note onparseSitemap.Content-Encoding: gzip) are decoded by undici as before and pass through unchanged..xml.gz)" section.Tests
test/sitemap.test.js, 5 new tests:application/x-gzipwith no Content-Encoding parses to all 5,000 entries; the raw bytes still failparseSitemap, which proves undici left them compressedResponse.text()wouldnode --testinpackages/plugin: 2,099 / 2,099 pass.eslintandprettier --checkon the touched files are clean..xml.gzchild decodes to 44,554 entries in 194 ms (decodeSitemapBody+parseSitemap).Verified live
The same two-file change was hot-patched onto a deployed 0.104.0 plugin (6-node cluster; the installed files matched the
prerender-v0.104.0tag byte for byte before patching). After an http_workers restart:POST /sitemaps/<index>withrevalidate: truefinished in about 20 s:sitemapsProcessed: 2/2,created: 46484,failed: [].created: 0.That cluster runs this patch outside a release until it upgrades to the release that contains it.
Version
0.106.1(patch). Open #254 has reserved0.107.0, so whichever merges second rebases its version.🤖 Generated with Claude Code