1
0

feat: 新增备用接口机制提升截图服务高可用性

当 mShots 返回无效图片或失败时,自动降级尝试使用 thum.io 获取截图。主要修改包括:
- 新增 fallback 请求处理逻辑
- 扩展缓存元数据记录来源信息
- 在响应头中添加 X-Source 标识备用接口
This commit is contained in:
2026-01-20 23:21:16 +08:00
parent 51f7d598d1
commit 07d9afdce5
2 changed files with 61 additions and 12 deletions

View File

@@ -53,3 +53,4 @@
- 添加详细的代码注释。
- 优化协议检测逻辑:当目标主机 443 端口无法连接或超时时,自动降级并打印日志 `falling back to HTTP`,确保非 SSL 站点也能正常访问。
- 优化缓存逻辑:不再缓存小尺寸(< 15KB的 GIF 图片(通常是 mShots 的 "Generating" 占位图),以便下次请求时能重新尝试获取真实截图。
- 新增备用接口机制:当 mShots 返回无效图片(如生成中 GIF或失败时自动降级尝试使用 `thum.io` 获取截图,确保高可用性。

View File

@@ -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);
});
// 简单根路由健康检查