feat: 新增备用接口机制提升截图服务高可用性
当 mShots 返回无效图片或失败时,自动降级尝试使用 thum.io 获取截图。主要修改包括: - 新增 fallback 请求处理逻辑 - 扩展缓存元数据记录来源信息 - 在响应头中添加 X-Source 标识备用接口
This commit is contained in:
72
server.js
72
server.js
@@ -9,6 +9,7 @@ require('dotenv').config();
|
||||
const app = express();
|
||||
const PORT = Number(process.env.PORT) || 11489;
|
||||
const UPSTREAM_HOST = 'https://s0.wp.com';
|
||||
const FALLBACK_HOST_BASE = 'https://image.thum.io/get/width/1024/crop/768/noanimate';
|
||||
const CACHE_DIR = path.join(process.cwd(), 'cache');
|
||||
|
||||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||
@@ -145,11 +146,37 @@ async function fetchUpstreamWithRetry(upstreamUrl, tries = 2) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心处理逻辑:检查缓存 -> 回源 -> 写入缓存 -> 返回响应
|
||||
* 请求备用接口 (thum.io)
|
||||
* @param {string} targetUrl
|
||||
* @returns {Promise<any>}
|
||||
*/
|
||||
async function fetchFallbackWithRetry(targetUrl) {
|
||||
// thum.io 格式: https://image.thum.io/get/<options>/<url>
|
||||
const fallbackUrl = `${FALLBACK_HOST_BASE}/${targetUrl}`;
|
||||
console.log(`[fallback-request] trying fallback: ${fallbackUrl}`);
|
||||
|
||||
try {
|
||||
return await axios.get(fallbackUrl, {
|
||||
responseType: 'arraybuffer',
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
|
||||
},
|
||||
timeout: 30000,
|
||||
validateStatus: () => true,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(`[fallback-error] msg=${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心处理逻辑:检查缓存 -> 回源 -> (失败则) 备用接口 -> 写入缓存 -> 返回响应
|
||||
* @param {object} res
|
||||
* @param {string} upstreamUrl
|
||||
* @param {string} targetUrl
|
||||
*/
|
||||
async function handleProxyRequest(res, upstreamUrl) {
|
||||
async function handleProxyRequest(res, upstreamUrl, targetUrl) {
|
||||
const key = sha1(upstreamUrl);
|
||||
const { data: dataPath, meta: metaPath } = getCachePaths(key);
|
||||
|
||||
@@ -176,27 +203,48 @@ async function handleProxyRequest(res, upstreamUrl) {
|
||||
// 2. 回源请求
|
||||
const resp = await fetchUpstreamWithRetry(upstreamUrl);
|
||||
|
||||
// 3. 仅缓存有效图片
|
||||
if (isValidImageResponse(resp.status, resp.headers, resp.data)) {
|
||||
const contentType = resp.headers['content-type'] || 'image/jpeg';
|
||||
// 3. 检查响应是否有效
|
||||
let finalResp = resp;
|
||||
let isFallback = false;
|
||||
|
||||
if (!isValidImageResponse(resp.status, resp.headers, resp.data)) {
|
||||
console.log(`[upstream-invalid] url=${upstreamUrl} status=${resp.status} len=${resp.data.byteLength}, trying fallback...`);
|
||||
const fallbackResp = await fetchFallbackWithRetry(targetUrl);
|
||||
|
||||
if (fallbackResp && isValidImageResponse(fallbackResp.status, fallbackResp.headers, fallbackResp.data)) {
|
||||
console.log(`[fallback-success] url=${targetUrl}`);
|
||||
finalResp = fallbackResp;
|
||||
isFallback = true;
|
||||
} else {
|
||||
console.log(`[fallback-failed] url=${targetUrl}, returning original response`);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 仅缓存有效图片 (无论是回源还是备用)
|
||||
if (isValidImageResponse(finalResp.status, finalResp.headers, finalResp.data)) {
|
||||
const contentType = finalResp.headers['content-type'] || 'image/jpeg';
|
||||
const meta = {
|
||||
url: upstreamUrl,
|
||||
url: isFallback ? `fallback:${targetUrl}` : upstreamUrl,
|
||||
contentType,
|
||||
size: resp.data.byteLength,
|
||||
size: finalResp.data.byteLength,
|
||||
createdAt: new Date().toISOString(),
|
||||
source: isFallback ? 'thum.io' : 'mshots'
|
||||
};
|
||||
try {
|
||||
fs.writeFileSync(dataPath, resp.data);
|
||||
fs.writeFileSync(dataPath, finalResp.data);
|
||||
fs.writeFileSync(metaPath, JSON.stringify(meta));
|
||||
} catch (e) {
|
||||
// 写盘失败也继续返回
|
||||
}
|
||||
res.type(contentType);
|
||||
res.set('Cache-Control', 'public, max-age=315360000, immutable');
|
||||
return res.send(resp.data);
|
||||
if (isFallback) {
|
||||
res.set('X-Source', 'fallback-thum.io');
|
||||
}
|
||||
return res.send(finalResp.data);
|
||||
}
|
||||
|
||||
// 4. 非图片或非200:兜底策略
|
||||
// 5. 非图片或非200:兜底策略
|
||||
// 若本地已有有效缓存(可能是旧的但有效),优先返回缓存
|
||||
if (fs.existsSync(dataPath) && fs.existsSync(metaPath)) {
|
||||
try {
|
||||
@@ -263,7 +311,7 @@ app.use('/mshots/v1', async (req, res) => {
|
||||
// 拼接完整上游 URL
|
||||
const upstreamUrl = UPSTREAM_HOST + '/mshots/v1/' + targetUrl;
|
||||
|
||||
return handleProxyRequest(res, upstreamUrl);
|
||||
return handleProxyRequest(res, upstreamUrl, targetUrl);
|
||||
});
|
||||
|
||||
// 根路径处理:/https://example.com 或 /www.baidu.com
|
||||
@@ -281,7 +329,7 @@ app.use(async (req, res) => {
|
||||
// 拼接完整上游 URL
|
||||
const upstreamUrl = UPSTREAM_HOST + '/mshots/v1/' + targetUrl;
|
||||
|
||||
return handleProxyRequest(res, upstreamUrl);
|
||||
return handleProxyRequest(res, upstreamUrl, targetUrl);
|
||||
});
|
||||
|
||||
// 简单根路由健康检查
|
||||
|
||||
Reference in New Issue
Block a user