@@ -12,6 +12,7 @@ import { defu } from 'defu'
1212import { getRequestHost } from 'h3'
1313import { parseURL } from 'ufo'
1414import { logger } from '../../../utils-pure'
15+ import { decodeSitemapResponseBytes } from './gzip'
1516
1617export function normalizeSourceInput ( source : SitemapSourceInput ) : SitemapSourceBase | SitemapSourceResolved {
1718 // string -> { fetch: string, context: { name: 'hook' } }
@@ -73,7 +74,11 @@ export async function fetchDataSource(input: SitemapSourceBase | SitemapSourceRe
7374
7475 try {
7576 let isMaybeErrorResponse = false
76- const isXmlRequest = parseURL ( url ) . pathname . endsWith ( '.xml' )
77+ const pathname = parseURL ( url ) . pathname . toLowerCase ( )
78+ // A `.gz` source (or a `.xml.gz` one) is still an XML sitemap once decompressed;
79+ // fetch it as bytes rather than assuming JSON.
80+ const isGzUrl = pathname . endsWith ( '.gz' )
81+ const isXmlRequest = pathname . endsWith ( '.xml' ) || isGzUrl
7782
7883 // Merge external source headers with request headers
7984 const mergedHeaders = defu (
@@ -86,7 +91,10 @@ export async function fetchDataSource(input: SitemapSourceBase | SitemapSourceRe
8691
8792 const fetchOptions = {
8893 ...options ,
89- responseType : isXmlRequest ? 'text' : 'json' ,
94+ // Fetch XML sources as raw bytes so we can detect and decompress a gzip body
95+ // (either a `.gz` URL, or a server that serves gzip without Content-Encoding)
96+ // before it's mangled by a UTF-8 text decode.
97+ responseType : isXmlRequest ? 'arrayBuffer' : 'json' ,
9098 signal : timeoutController . signal ,
9199 headers : mergedHeaders ,
92100 // Use ofetch's built-in retry for external sources
@@ -114,13 +122,24 @@ export async function fetchDataSource(input: SitemapSourceBase | SitemapSourceRe
114122 }
115123 }
116124 let urls = [ ]
117- if ( typeof res === 'object' ) {
118- urls = res . urls || res
119- }
120- else if ( typeof res === 'string' && isXmlRequest ) {
121- const result = await parseSitemapXml ( res )
125+ if ( isXmlRequest ) {
126+ const bytes = res instanceof Uint8Array ? res : new Uint8Array ( res as ArrayBuffer )
127+ const text = await decodeSitemapResponseBytes ( bytes )
128+ if ( text . startsWith ( '<!DOCTYPE html>' ) ) {
129+ return {
130+ ...input ,
131+ context,
132+ urls : [ ] ,
133+ timeTakenMs,
134+ error : 'Received HTML response instead of XML' ,
135+ }
136+ }
137+ const result = await parseSitemapXml ( text )
122138 urls = result . urls
123139 }
140+ else if ( typeof res === 'object' ) {
141+ urls = res . urls || res
142+ }
124143 return {
125144 ...input ,
126145 context,
0 commit comments