mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-13 15:53:44 +08:00
101 lines
4.1 KiB
TypeScript
101 lines
4.1 KiB
TypeScript
import { NextRequest } from 'next/server';
|
|
|
|
interface FetchWithRetryOptions {
|
|
url: string;
|
|
request: NextRequest;
|
|
headers?: Record<string, string>;
|
|
}
|
|
|
|
export async function fetchWithRetry({ url, request, headers = {} }: FetchWithRetryOptions): Promise<Response> {
|
|
// User-Agent rotation for better compatibility
|
|
const userAgents = [
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1',
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0'
|
|
];
|
|
const randomUA = userAgents[Math.floor(Math.random() * userAgents.length)];
|
|
|
|
// Smart Referer: use video domain instead of kvideo.vercel.app to avoid suspicion
|
|
const videoUrl = new URL(url);
|
|
const referer = request.nextUrl.searchParams.get('referer') || `${videoUrl.protocol}//${videoUrl.hostname}`;
|
|
|
|
// Optional IP forwarding (default: Beijing IP)
|
|
const forwardedIP = request.nextUrl.searchParams.get('ip') || '202.108.22.5';
|
|
|
|
const MAX_RETRIES = 5;
|
|
const TIMEOUT_MS = 30000; // 30 seconds
|
|
let lastError: unknown = null;
|
|
let response: Response | null = null;
|
|
|
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
try {
|
|
// Exponential backoff: 100ms, 200ms, 400ms, 800ms, 1600ms
|
|
const backoffDelay = attempt > 1 ? Math.pow(2, attempt - 2) * 100 : 0;
|
|
if (backoffDelay > 0) {
|
|
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
|
}
|
|
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
|
|
response = await fetch(url, {
|
|
headers: {
|
|
...headers, // First: forwarded headers (Cookie, Range)
|
|
// Then override with anti-blocking headers (these take precedence)
|
|
'User-Agent': randomUA,
|
|
'Accept': '*/*',
|
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
'Accept-Encoding': 'gzip, deflate, br',
|
|
'Connection': 'keep-alive',
|
|
'X-Forwarded-For': forwardedIP,
|
|
'Client-IP': forwardedIP,
|
|
'Referer': referer,
|
|
'Origin': `${videoUrl.protocol}//${videoUrl.hostname}`,
|
|
'Sec-Fetch-Dest': 'empty',
|
|
'Sec-Fetch-Mode': 'cors',
|
|
'Sec-Fetch-Site': 'cross-site',
|
|
},
|
|
signal: controller.signal,
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
if (response.ok) {
|
|
break;
|
|
}
|
|
|
|
if (response.status === 503 && attempt < MAX_RETRIES) {
|
|
console.warn(`⚠ Got 503 on attempt ${attempt}, retrying with backoff ${backoffDelay}ms...`);
|
|
lastError = `503 on attempt ${attempt}`;
|
|
continue;
|
|
}
|
|
|
|
console.warn(`✗ Got ${response.status} on attempt ${attempt}`);
|
|
break;
|
|
} catch (fetchError) {
|
|
lastError = fetchError;
|
|
if (fetchError instanceof Error && fetchError.name === 'AbortError') {
|
|
console.warn(`⚠ Timeout on attempt ${attempt}, retrying...`);
|
|
} else if (attempt < MAX_RETRIES) {
|
|
console.warn(`⚠ Fetch error on attempt ${attempt}, retrying...`, fetchError);
|
|
} else {
|
|
throw fetchError;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we got a response (even an error response like 403, 404), return it
|
|
// Only throw if we truly failed to get any response
|
|
if (!response) {
|
|
throw new Error(`Failed after ${MAX_RETRIES} attempts: ${lastError}`);
|
|
}
|
|
|
|
// Return the response even if it's an error status (403, 404, etc.)
|
|
// The caller can check response.ok or response.status
|
|
if (!response.ok) {
|
|
console.warn(`⚠ Returning non-OK response: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
return response;
|
|
}
|