mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
fix(douban): 图片代理按镜像回退,修复境外部署海报大量 500
imgN.doubanio.com 互为镜像但解析到不同线路:img1 当前解析到国内 IP 180.97.198.41,在境外主机上不可达。此时 fetch 是直接抛异常而不是返回 状态码,于是走进 catch 一律返回 500,首页会有相当比例的海报加载失败。 改为在 imgN.doubanio.com 之间按 img9 -> img3 -> img2 顺序回退,同一 路径换个镜像即可取到。同时保留最后一次的真实状态码,不再把上游的 403 或 404 也统一伪装成 500。
This commit is contained in:
@@ -2,6 +2,40 @@ import { NextResponse } from 'next/server';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
const REQUEST_HEADERS = {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
||||
Accept: 'image/jpeg,image/png,image/gif,*/*;q=0.8',
|
||||
Referer: 'https://movie.douban.com/',
|
||||
};
|
||||
|
||||
// 豆瓣的 imgN.doubanio.com 互为镜像,但各自解析到不同线路。部分线路(例如 img1
|
||||
// 的国内 IP)在境外主机上不可达,fetch 会直接抛错而不是返回状态码,海报因此变成
|
||||
// 500。同一路径换个镜像通常就能取到,所以失败时按顺序回退。
|
||||
const DOUBAN_IMAGE_HOSTS = ['img9.doubanio.com', 'img3.doubanio.com', 'img2.doubanio.com'];
|
||||
|
||||
function buildCandidates(rawUrl: string): string[] {
|
||||
const candidates = [rawUrl];
|
||||
|
||||
try {
|
||||
const parsed = new URL(rawUrl);
|
||||
if (!/^img\d+\.doubanio\.com$/.test(parsed.hostname)) {
|
||||
return candidates;
|
||||
}
|
||||
|
||||
for (const host of DOUBAN_IMAGE_HOSTS) {
|
||||
if (host === parsed.hostname) continue;
|
||||
const alternate = new URL(parsed.toString());
|
||||
alternate.hostname = host;
|
||||
candidates.push(alternate.toString());
|
||||
}
|
||||
} catch {
|
||||
// 非法 URL 留给 fetch 去报错
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const imageUrl = searchParams.get('url');
|
||||
@@ -10,51 +44,43 @@ export async function GET(request: Request) {
|
||||
return NextResponse.json({ error: 'Missing image URL' }, { status: 400 });
|
||||
}
|
||||
|
||||
let lastStatus = 502;
|
||||
let lastError = 'Error fetching image';
|
||||
|
||||
for (const candidate of buildCandidates(imageUrl)) {
|
||||
let imageResponse: Response;
|
||||
|
||||
try {
|
||||
const imageResponse = await fetch(imageUrl, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36',
|
||||
Accept: 'image/jpeg,image/png,image/gif,*/*;q=0.8',
|
||||
Referer: 'https://movie.douban.com/',
|
||||
},
|
||||
});
|
||||
imageResponse = await fetch(candidate, { headers: REQUEST_HEADERS });
|
||||
} catch {
|
||||
// 线路不可达,换下一个镜像
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!imageResponse.ok) {
|
||||
return NextResponse.json(
|
||||
{ error: imageResponse.statusText },
|
||||
{ status: imageResponse.status }
|
||||
);
|
||||
lastStatus = imageResponse.status;
|
||||
lastError = imageResponse.statusText || 'Error fetching image';
|
||||
continue;
|
||||
}
|
||||
|
||||
const contentType = imageResponse.headers.get('content-type');
|
||||
|
||||
if (!imageResponse.body) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Image response has no body' },
|
||||
{ status: 500 }
|
||||
);
|
||||
lastStatus = 500;
|
||||
lastError = 'Image response has no body';
|
||||
continue;
|
||||
}
|
||||
|
||||
// 创建响应头
|
||||
const headers = new Headers();
|
||||
const contentType = imageResponse.headers.get('content-type');
|
||||
if (contentType) {
|
||||
headers.set('Content-Type', contentType);
|
||||
}
|
||||
|
||||
// 设置缓存头
|
||||
headers.set('Cache-Control', 'public, max-age=15720000, s-maxage=15720000');
|
||||
|
||||
// 直接返回图片流
|
||||
// @ts-ignore
|
||||
return new Response(imageResponse.body, {
|
||||
status: 200,
|
||||
headers,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Error fetching image' },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return NextResponse.json({ error: lastError }, { status: lastStatus });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user