@@ -23,7 +23,11 @@ type ModuleEntry = {
2323type 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(
4650const MODULES_DIR = path . join ( PROJECT_ROOT , "modules" ) ;
4751const 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
5261type 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+
170229async 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) {
255363function 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