Skip to content

Commit f4e8a87

Browse files
fix(Crowdin): Handle gzip CDN responses in fetchUrl (#226)
1 parent 7b3f83e commit f4e8a87

1 file changed

Lines changed: 34 additions & 13 deletions

File tree

.github/scripts/sync-crowdin-distribution.js

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
const https = require('node:https');
1616
const fs = require('node:fs');
1717
const path = require('node:path');
18+
const zlib = require('node:zlib');
19+
const { promisify } = require('node:util');
20+
21+
const gunzip = promisify(zlib.gunzip);
1822

1923
const BASE_CDN = 'https://distributions.crowdin.net';
2024
const OUTPUT_DIR = path.resolve(process.env.OUTPUT_DIR || 'dist-pages/crowdin-dist');
@@ -38,27 +42,44 @@ if (DISTRIBUTIONS.length === 0) {
3842
process.exit(1);
3943
}
4044

45+
/**
46+
* Collects all data chunks from an HTTP response stream into a single Buffer.
47+
* @param {import('http').IncomingMessage} res
48+
* @returns {Promise<Buffer>}
49+
*/
50+
function collectBody(res) {
51+
return new Promise((resolve, reject) => {
52+
const chunks = [];
53+
res.on('data', (chunk) => chunks.push(chunk));
54+
res.on('end', () => resolve(Buffer.concat(chunks)));
55+
res.on('error', reject);
56+
});
57+
}
58+
4159
/**
4260
* Fetches a URL, following redirects, and returns the body as a Buffer.
61+
* Transparently decompresses gzip-encoded responses so callers always receive
62+
* plain bytes (the Crowdin CDN stores content files with Content-Encoding: gzip,
63+
* but jsDelivr re-serves the raw bytes without that header).
4364
* @param {string} url
4465
* @returns {Promise<Buffer>}
4566
*/
46-
function fetchUrl(url) {
67+
async function fetchUrl(url) {
4768
return new Promise((resolve, reject) => {
48-
https.get(url, (res) => {
49-
// Follow redirects
69+
https.get(url, async (res) => {
5070
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
51-
return fetchUrl(res.headers.location).then(resolve).catch(reject);
71+
return fetchUrl(res.headers.location).then(resolve, reject);
72+
}
73+
if (res.statusCode >= 400) {
74+
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
75+
}
76+
try {
77+
const buffer = await collectBody(res);
78+
const isGzip = res.headers['content-encoding'] === 'gzip';
79+
resolve(isGzip ? await gunzip(buffer) : buffer);
80+
} catch (err) {
81+
reject(err);
5282
}
53-
const chunks = [];
54-
res.on('data', (chunk) => chunks.push(chunk));
55-
res.on('end', () => {
56-
if (res.statusCode >= 400) {
57-
return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
58-
}
59-
resolve(Buffer.concat(chunks));
60-
});
61-
res.on('error', reject);
6283
}).on('error', reject);
6384
});
6485
}

0 commit comments

Comments
 (0)