feat: 新增备用接口机制提升截图服务高可用性
当 mShots 返回无效图片或失败时,自动降级尝试使用 thum.io 获取截图。主要修改包括: - 新增 fallback 请求处理逻辑 - 扩展缓存元数据记录来源信息 - 在响应头中添加 X-Source 标识备用接口
This commit is contained in:
@@ -53,3 +53,4 @@
|
|||||||
- 添加详细的代码注释。
|
- 添加详细的代码注释。
|
||||||
- 优化协议检测逻辑:当目标主机 443 端口无法连接或超时时,自动降级并打印日志 `falling back to HTTP`,确保非 SSL 站点也能正常访问。
|
- 优化协议检测逻辑:当目标主机 443 端口无法连接或超时时,自动降级并打印日志 `falling back to HTTP`,确保非 SSL 站点也能正常访问。
|
||||||
- 优化缓存逻辑:不再缓存小尺寸(< 15KB)的 GIF 图片(通常是 mShots 的 "Generating" 占位图),以便下次请求时能重新尝试获取真实截图。
|
- 优化缓存逻辑:不再缓存小尺寸(< 15KB)的 GIF 图片(通常是 mShots 的 "Generating" 占位图),以便下次请求时能重新尝试获取真实截图。
|
||||||
|
- 新增备用接口机制:当 mShots 返回无效图片(如生成中 GIF)或失败时,自动降级尝试使用 `thum.io` 获取截图,确保高可用性。
|
||||||
|
|||||||
72
server.js
72
server.js
@@ -9,6 +9,7 @@ require('dotenv').config();
|
|||||||
const app = express();
|
const app = express();
|
||||||
const PORT = Number(process.env.PORT) || 11489;
|
const PORT = Number(process.env.PORT) || 11489;
|
||||||
const UPSTREAM_HOST = 'https://s0.wp.com';
|
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');
|
const CACHE_DIR = path.join(process.cwd(), 'cache');
|
||||||
|
|
||||||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
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 {object} res
|
||||||
* @param {string} upstreamUrl
|
* @param {string} upstreamUrl
|
||||||
|
* @param {string} targetUrl
|
||||||
*/
|
*/
|
||||||
async function handleProxyRequest(res, upstreamUrl) {
|
async function handleProxyRequest(res, upstreamUrl, targetUrl) {
|
||||||
const key = sha1(upstreamUrl);
|
const key = sha1(upstreamUrl);
|
||||||
const { data: dataPath, meta: metaPath } = getCachePaths(key);
|
const { data: dataPath, meta: metaPath } = getCachePaths(key);
|
||||||
|
|
||||||
@@ -176,27 +203,48 @@ async function handleProxyRequest(res, upstreamUrl) {
|
|||||||
// 2. 回源请求
|
// 2. 回源请求
|
||||||
const resp = await fetchUpstreamWithRetry(upstreamUrl);
|
const resp = await fetchUpstreamWithRetry(upstreamUrl);
|
||||||
|
|
||||||
// 3. 仅缓存有效图片
|
// 3. 检查响应是否有效
|
||||||
if (isValidImageResponse(resp.status, resp.headers, resp.data)) {
|
let finalResp = resp;
|
||||||
const contentType = resp.headers['content-type'] || 'image/jpeg';
|
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 = {
|
const meta = {
|
||||||
url: upstreamUrl,
|
url: isFallback ? `fallback:${targetUrl}` : upstreamUrl,
|
||||||
contentType,
|
contentType,
|
||||||
size: resp.data.byteLength,
|
size: finalResp.data.byteLength,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
|
source: isFallback ? 'thum.io' : 'mshots'
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
fs.writeFileSync(dataPath, resp.data);
|
fs.writeFileSync(dataPath, finalResp.data);
|
||||||
fs.writeFileSync(metaPath, JSON.stringify(meta));
|
fs.writeFileSync(metaPath, JSON.stringify(meta));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// 写盘失败也继续返回
|
// 写盘失败也继续返回
|
||||||
}
|
}
|
||||||
res.type(contentType);
|
res.type(contentType);
|
||||||
res.set('Cache-Control', 'public, max-age=315360000, immutable');
|
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)) {
|
if (fs.existsSync(dataPath) && fs.existsSync(metaPath)) {
|
||||||
try {
|
try {
|
||||||
@@ -263,7 +311,7 @@ app.use('/mshots/v1', async (req, res) => {
|
|||||||
// 拼接完整上游 URL
|
// 拼接完整上游 URL
|
||||||
const upstreamUrl = UPSTREAM_HOST + '/mshots/v1/' + targetUrl;
|
const upstreamUrl = UPSTREAM_HOST + '/mshots/v1/' + targetUrl;
|
||||||
|
|
||||||
return handleProxyRequest(res, upstreamUrl);
|
return handleProxyRequest(res, upstreamUrl, targetUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 根路径处理:/https://example.com 或 /www.baidu.com
|
// 根路径处理:/https://example.com 或 /www.baidu.com
|
||||||
@@ -281,7 +329,7 @@ app.use(async (req, res) => {
|
|||||||
// 拼接完整上游 URL
|
// 拼接完整上游 URL
|
||||||
const upstreamUrl = UPSTREAM_HOST + '/mshots/v1/' + targetUrl;
|
const upstreamUrl = UPSTREAM_HOST + '/mshots/v1/' + targetUrl;
|
||||||
|
|
||||||
return handleProxyRequest(res, upstreamUrl);
|
return handleProxyRequest(res, upstreamUrl, targetUrl);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 简单根路由健康检查
|
// 简单根路由健康检查
|
||||||
|
|||||||
Reference in New Issue
Block a user