File size: 2,714 Bytes
a6b6c66
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import { lookup } from 'node:dns/promises';

/**
 * Enhanced fetch with retries, timeouts, and DNS diagnostics.
 * Specifically designed to handle Hugging Face egress limitations.
 */
export async function fetchWithRetry(
  url: string, 
  options: any = {}, 
  retries = 7, 
  backoff = 3000
): Promise<any> {
  const isServer = typeof window === 'undefined';

  for (let i = 0; i < retries; i++) {
    try {
      if (isServer) console.log(`[fetchWithRetry] Attempt ${i + 1}: ${url.split('?')[0]}...`);
      
      const res = await fetch(url, {
        ...options,
        signal: AbortSignal.timeout(options.timeout || 30000),
        headers: {
          'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
          'Accept': 'application/json',
          'Accept-Language': 'en-US,en;q=0.9',
          ...options.headers
        }
      });

      if (res.ok) {
        // Automatically parse JSON for convenience in shared utilities
        const text = await res.text();
        if (!text) return {};
        try {
          return JSON.parse(text);
        } catch {
          return { raw: text };
        }
      }

      const errorBody = await res.clone().text().catch(() => "No error body");
      console.warn(`[fetchWithRetry] Status ${res.status}: ${errorBody.substring(0, 150)}`);

      if (i < retries - 1 && (res.status === 429 || res.status >= 500)) {
        await new Promise(r => setTimeout(r, backoff * (i + 1)));
        continue;
      }
      
      // If we got an error but it's not retryable (e.g. 400), throw it
      const errorData = await res.json().catch(() => ({}));
      throw new Error(errorData?.error?.message || `API Error ${res.status}`);
      
    } catch (err: any) {
      if (isServer && i > 0) {
        try {
          const host = new URL(url).hostname;
          const addr = await lookup(host);
          console.log(`[fetchWithRetry] DNS Diagnostic for ${host}: ${addr.address} (${addr.family})`);
        } catch (dnsErr: any) {
          console.warn(`[fetchWithRetry] DNS Diagnostic Failed: ${dnsErr.message}`);
        }
      }

      if (i === retries - 1) throw err;
      
      const isNetworkError = 
        err.name === 'AbortError' || 
        err.name === 'TimeoutError' || 
        err.message.includes('fetch failed') || 
        err.message.includes('timeout') ||
        err.code === 'UND_ERR_CONNECT_TIMEOUT';

      if (isNetworkError) {
        if (isServer) console.warn(`[fetchWithRetry] Network Error: ${err.message}. Retrying...`);
        await new Promise(r => setTimeout(r, backoff * (i + 1)));
        continue;
      }

      throw err;
    }
  }
}