Skip to content

Commit dae1a80

Browse files
feat:iImprove get-modules URL validation resilience and logging
1 parent 7386aa7 commit dae1a80

File tree

1 file changed

+155
-15
lines changed

1 file changed

+155
-15
lines changed

scripts/get-modules.ts

Lines changed: 155 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ type ModuleEntry = {
2323
type UrlValidationResult = {
2424
module: ModuleEntry;
2525
statusCode: number | null;
26+
statusText?: string;
2627
ok: boolean;
28+
usedFallback?: boolean;
29+
initialStatusCode?: number | null;
30+
responseSnippet?: string;
2731
error?: string;
2832
};
2933

@@ -46,8 +50,13 @@ const SKIPPED_MODULES_PATH = path.join(
4650
const MODULES_DIR = path.join(PROJECT_ROOT, "modules");
4751
const MODULES_TEMP_DIR = path.join(PROJECT_ROOT, "modules_temp");
4852

49-
const DEFAULT_URL_CONCURRENCY = 20;
50-
const DEFAULT_URL_RATE = 60;
53+
const DEFAULT_URL_CONCURRENCY = 10;
54+
const DEFAULT_URL_RATE = 15;
55+
const URL_VALIDATION_RETRY_COUNT = 5;
56+
const URL_VALIDATION_RETRY_DELAY_MS = 3000;
57+
const RESPONSE_SNIPPET_MAX_LENGTH = 512;
58+
const RESPONSE_SNIPPET_LOG_LENGTH = 200;
59+
const REDIRECT_STATUS_CODES = new Set([301, 302, 307, 308]);
5160

5261
type CliOptions = {
5362
limit?: number;
@@ -108,9 +117,7 @@ function parseCliOptions(argv: string[]): CliOptions {
108117
return {
109118
limit,
110119
urlConcurrency: normalizedConcurrency,
111-
urlRate: disableRateLimiter
112-
? undefined
113-
: (parsedRate ?? Math.max(normalizedConcurrency * 2, DEFAULT_URL_RATE))
120+
urlRate: disableRateLimiter ? undefined : (parsedRate ?? DEFAULT_URL_RATE)
114121
};
115122
}
116123

@@ -167,25 +174,126 @@ function extractOwnerFromUrl(url: string): string {
167174
}
168175
}
169176

177+
function isSuccessStatus(status: number | null | undefined) {
178+
return typeof status === "number" && status >= 200 && status < 300;
179+
}
180+
181+
function isAllowedRedirect(status: number | null | undefined) {
182+
return typeof status === "number" && REDIRECT_STATUS_CODES.has(status);
183+
}
184+
185+
function formatSnippetForLog(snippet?: string) {
186+
if (!snippet) {
187+
return "<empty response>";
188+
}
189+
190+
return snippet
191+
.replace(/\s+/g, " ")
192+
.trim()
193+
.slice(0, RESPONSE_SNIPPET_LOG_LENGTH);
194+
}
195+
196+
async function readSnippet(response: Response) {
197+
try {
198+
const text = await response.text();
199+
if (!text) {
200+
return undefined;
201+
}
202+
return text.slice(0, RESPONSE_SNIPPET_MAX_LENGTH);
203+
} catch {
204+
return undefined;
205+
}
206+
}
207+
208+
async function fetchFallbackPreview(url: string) {
209+
try {
210+
const response = await httpClient.request(url, {
211+
method: "GET",
212+
redirect: "manual",
213+
retries: 1,
214+
retryDelayMs: URL_VALIDATION_RETRY_DELAY_MS
215+
});
216+
const snippet = await readSnippet(response);
217+
return {
218+
statusCode: response.status,
219+
statusText: response.statusText,
220+
snippet
221+
};
222+
} catch (error) {
223+
const message = error instanceof Error ? error.message : String(error);
224+
logger.debug(`Fallback GET failed for ${url}: ${message}`);
225+
return undefined;
226+
}
227+
}
228+
170229
async function validateModuleUrl(
171230
module: ModuleEntry
172231
): Promise<UrlValidationResult> {
173232
try {
174-
const response = await httpClient.request(module.url, {
233+
const headResponse = await httpClient.request(module.url, {
175234
method: "HEAD",
176-
redirect: "manual"
235+
redirect: "manual",
236+
retries: URL_VALIDATION_RETRY_COUNT,
237+
retryDelayMs: URL_VALIDATION_RETRY_DELAY_MS
177238
});
239+
240+
const headSnippet = await readSnippet(headResponse);
241+
const headStatus = headResponse.status;
242+
const headStatusText = headResponse.statusText;
243+
244+
if (isSuccessStatus(headStatus) || isAllowedRedirect(headStatus)) {
245+
return {
246+
module,
247+
statusCode: headStatus,
248+
statusText: headStatusText,
249+
ok: true,
250+
responseSnippet: headSnippet
251+
};
252+
}
253+
254+
const fallbackPreview = await fetchFallbackPreview(module.url);
255+
if (
256+
fallbackPreview &&
257+
(isSuccessStatus(fallbackPreview.statusCode) ||
258+
isAllowedRedirect(fallbackPreview.statusCode))
259+
) {
260+
logger.warn(
261+
`URL ${module.url} rejected HEAD (${headStatus} ${headStatusText}) but accepted fallback GET (${fallbackPreview.statusCode} ${fallbackPreview.statusText}).`
262+
);
263+
return {
264+
module,
265+
statusCode: fallbackPreview.statusCode ?? headStatus,
266+
statusText: fallbackPreview.statusText ?? headStatusText,
267+
ok: true,
268+
usedFallback: true,
269+
initialStatusCode: headStatus,
270+
responseSnippet: fallbackPreview.snippet ?? headSnippet
271+
};
272+
}
273+
274+
const snippet = fallbackPreview?.snippet ?? headSnippet;
275+
const finalStatusCode = fallbackPreview?.statusCode ?? headStatus;
276+
const finalStatusText = fallbackPreview?.statusText ?? headStatusText;
277+
278+
logger.warn(
279+
`URL ${module.url} failed validation (${finalStatusCode} ${finalStatusText}). Sample: ${formatSnippetForLog(snippet)}`
280+
);
281+
178282
return {
179283
module,
180-
statusCode: response.status,
181-
ok: response.status === 200 || response.status === 301
284+
statusCode: finalStatusCode,
285+
statusText: finalStatusText,
286+
ok: false,
287+
initialStatusCode: headStatus,
288+
responseSnippet: snippet
182289
};
183290
} catch (error) {
184291
const message = error instanceof Error ? error.message : String(error);
185292
logger.warn(`URL ${module.url}: ${message}`);
186293
return {
187294
module,
188295
statusCode: null,
296+
statusText: undefined,
189297
ok: false,
190298
error: message
191299
};
@@ -255,17 +363,27 @@ function ensureIssueArray(module: ModuleEntry) {
255363
function createSkippedEntry(
256364
module: ModuleEntry,
257365
error: string,
258-
errorType: string
366+
errorType: string,
367+
details: {
368+
statusCode?: number | null;
369+
statusText?: string;
370+
responseSnippet?: string;
371+
initialStatusCode?: number | null;
372+
} = {}
259373
) {
260374
const owner = extractOwnerFromUrl(module.url);
375+
const normalizedDetails = Object.fromEntries(
376+
Object.entries(details).filter(([, value]) => value !== undefined)
377+
);
261378
return {
262379
name: module.name,
263380
url: module.url,
264381
maintainer: owner,
265382
description:
266383
typeof module.description === "string" ? module.description : "",
267384
error,
268-
errorType
385+
errorType,
386+
...normalizedDetails
269387
};
270388
}
271389

@@ -359,10 +477,23 @@ async function processModules() {
359477

360478
await ensureDirectory(MODULES_DIR);
361479

362-
for (const { module, ok, statusCode } of validated) {
480+
for (const {
481+
module,
482+
ok,
483+
statusCode,
484+
statusText,
485+
responseSnippet,
486+
usedFallback,
487+
initialStatusCode
488+
} of validated) {
363489
if (!ok) {
364490
skippedModules.push(
365-
createSkippedEntry(module, "Invalid repository URL", "invalid_url")
491+
createSkippedEntry(module, "Invalid repository URL", "invalid_url", {
492+
statusCode,
493+
statusText,
494+
responseSnippet,
495+
initialStatusCode
496+
})
366497
);
367498
continue;
368499
}
@@ -374,10 +505,19 @@ async function processModules() {
374505

375506
const moduleCopy: ModuleEntry = { ...module };
376507

377-
if (statusCode === 301) {
508+
if (statusCode && REDIRECT_STATUS_CODES.has(statusCode)) {
509+
ensureIssueArray(moduleCopy);
510+
moduleCopy.issues?.push(
511+
statusCode === 301
512+
? "The repository URL returns a 301 status code, indicating it has been moved. Please verify the new location and update the module list if necessary."
513+
: `The repository URL returned a ${statusCode} redirect during validation. Please confirm the final destination and update the module list if necessary.`
514+
);
515+
}
516+
517+
if (usedFallback) {
378518
ensureIssueArray(moduleCopy);
379519
moduleCopy.issues?.push(
380-
"The repository URL returns a 301 status code, indicating it has been moved. Please verify the new location and update the module list if necessary."
520+
"HEAD requests to this repository failed but a subsequent GET request succeeded. Please verify the repository URL and server configuration."
381521
);
382522
}
383523

0 commit comments

Comments
 (0)