Files
droid2api/server.js
empty 3dccbcfed1 feat: 添加全局错误处理机制
- 添加 unhandledRejection 处理器捕获未处理的 Promise rejection
- 添加 uncaughtException 处理器捕获未捕获的异常
- 添加 SIGTERM/SIGINT 信号处理实现优雅关闭
- 实现 gracefulShutdown 函数,给正在处理的请求3秒完成时间
- 错误信息经过 sanitizeForLog 脱敏处理
- 生产环境下隐藏堆栈信息

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-27 15:24:03 +08:00

262 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import express from 'express';
import { loadConfig, isDevMode, getPort } from './config.js';
import { logInfo, logError } from './logger.js';
import router from './routes.js';
import { initializeAuth } from './auth.js';
import { initializeUserAgentUpdater } from './user-agent-updater.js';
import './sls-logger.js'; // 初始化阿里云日志服务
import { sanitizeForLog } from './log-sanitizer.js';
// ============================================================================
// 全局错误处理 - 必须在应用启动前注册
// ============================================================================
let isShuttingDown = false;
/**
* 优雅关闭服务器
*/
function gracefulShutdown(reason, exitCode = 1) {
if (isShuttingDown) {
return;
}
isShuttingDown = true;
console.error(`\n${'='.repeat(80)}`);
console.error(`🛑 Server shutting down: ${reason}`);
console.error(`${'='.repeat(80)}\n`);
// 给正在处理的请求一些时间完成
setTimeout(() => {
process.exit(exitCode);
}, 3000);
}
/**
* 处理未捕获的 Promise Rejection
*/
process.on('unhandledRejection', (reason, promise) => {
const errorInfo = {
type: 'unhandledRejection',
reason: reason instanceof Error ? reason.message : String(reason),
stack: reason instanceof Error ? reason.stack : undefined,
timestamp: new Date().toISOString()
};
console.error(`\n${'='.repeat(80)}`);
console.error('⚠️ Unhandled Promise Rejection');
console.error('='.repeat(80));
console.error(`Time: ${errorInfo.timestamp}`);
console.error(`Reason: ${errorInfo.reason}`);
if (errorInfo.stack && isDevMode()) {
console.error(`Stack: ${errorInfo.stack}`);
}
console.error('='.repeat(80) + '\n');
logError('Unhandled Promise Rejection', sanitizeForLog(errorInfo));
// 在生产环境中unhandledRejection 可能表示严重问题,考虑退出
// 但为了服务稳定性,这里只记录不退出
// 如需更严格的处理,可取消下面的注释:
// gracefulShutdown('Unhandled Promise Rejection', 1);
});
/**
* 处理未捕获的异常
*/
process.on('uncaughtException', (error) => {
const errorInfo = {
type: 'uncaughtException',
message: error.message,
stack: error.stack,
timestamp: new Date().toISOString()
};
console.error(`\n${'='.repeat(80)}`);
console.error('💥 Uncaught Exception');
console.error('='.repeat(80));
console.error(`Time: ${errorInfo.timestamp}`);
console.error(`Error: ${errorInfo.message}`);
if (errorInfo.stack) {
console.error(`Stack: ${errorInfo.stack}`);
}
console.error('='.repeat(80) + '\n');
logError('Uncaught Exception', sanitizeForLog(errorInfo));
// uncaughtException 后进程状态不确定,必须退出
gracefulShutdown('Uncaught Exception', 1);
});
/**
* 处理进程信号
*/
process.on('SIGTERM', () => {
logInfo('Received SIGTERM signal');
gracefulShutdown('SIGTERM', 0);
});
process.on('SIGINT', () => {
logInfo('Received SIGINT signal');
gracefulShutdown('SIGINT', 0);
});
// ============================================================================
const app = express();
app.use(express.json({ limit: '50mb' }));
app.use(express.urlencoded({ extended: true, limit: '50mb' }));
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key, anthropic-version');
if (req.method === 'OPTIONS') {
return res.sendStatus(200);
}
next();
});
app.use(router);
app.get('/', (req, res) => {
res.json({
name: 'droid2api',
version: '1.0.0',
description: 'OpenAI Compatible API Proxy',
endpoints: [
'GET /v1/models',
'POST /v1/chat/completions',
'POST /v1/responses',
'POST /v1/messages',
'POST /v1/messages/count_tokens'
]
});
});
// 404 处理 - 捕获所有未匹配的路由
app.use((req, res, next) => {
const errorInfo = {
timestamp: new Date().toISOString(),
method: req.method,
url: req.originalUrl || req.url,
path: req.path,
query: req.query,
params: req.params,
body: req.body,
headers: {
'content-type': req.headers['content-type'],
'user-agent': req.headers['user-agent'],
'origin': req.headers['origin'],
'referer': req.headers['referer']
},
ip: req.ip || req.connection.remoteAddress
};
const safeQuery = sanitizeForLog(errorInfo.query);
const safeBody = sanitizeForLog(errorInfo.body);
const safeHeaders = sanitizeForLog(errorInfo.headers);
const safeIp = sanitizeForLog(errorInfo.ip);
console.error('\n' + '='.repeat(80));
console.error('❌ 非法请求地址');
console.error('='.repeat(80));
console.error(`时间: ${errorInfo.timestamp}`);
console.error(`方法: ${errorInfo.method}`);
console.error(`地址: ${errorInfo.url}`);
console.error(`路径: ${errorInfo.path}`);
if (Object.keys(errorInfo.query).length > 0) {
console.error(`查询参数: ${JSON.stringify(safeQuery, null, 2)}`);
}
if (errorInfo.body && Object.keys(errorInfo.body).length > 0) {
console.error(`请求体: ${JSON.stringify(safeBody, null, 2)}`);
}
console.error(`客户端IP: ${safeIp}`);
console.error(`User-Agent: ${safeHeaders['user-agent'] || 'N/A'}`);
if (safeHeaders.referer) {
console.error(`来源: ${safeHeaders.referer}`);
}
console.error('='.repeat(80) + '\n');
logError('Invalid request path', sanitizeForLog(errorInfo));
res.status(404).json({
error: 'Not Found',
message: `路径 ${req.method} ${req.path} 不存在`,
timestamp: errorInfo.timestamp,
availableEndpoints: [
'GET /v1/models',
'POST /v1/chat/completions',
'POST /v1/responses',
'POST /v1/messages',
'POST /v1/messages/count_tokens'
]
});
});
// 错误处理中间件
app.use((err, req, res, next) => {
logError('Unhandled error', err);
res.status(500).json({
error: 'Internal server error',
message: isDevMode() ? err.message : undefined
});
});
(async () => {
try {
loadConfig();
logInfo('Configuration loaded successfully');
logInfo(`Dev mode: ${isDevMode()}`);
// Initialize User-Agent version updater
initializeUserAgentUpdater();
// Initialize auth system (load and setup API key if needed)
// This won't throw error if no auth config is found - will use client auth
await initializeAuth();
const PORT = getPort();
logInfo(`Starting server on port ${PORT}...`);
const server = app.listen(PORT)
.on('listening', () => {
logInfo(`Server running on http://localhost:${PORT}`);
logInfo('Available endpoints:');
logInfo(' GET /v1/models');
logInfo(' POST /v1/chat/completions');
logInfo(' POST /v1/responses');
logInfo(' POST /v1/messages');
logInfo(' POST /v1/messages/count_tokens');
})
.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.error(`\n${'='.repeat(80)}`);
console.error(`ERROR: Port ${PORT} is already in use!`);
console.error('');
console.error('Please choose one of the following options:');
console.error(` 1. Stop the process using port ${PORT}:`);
console.error(` lsof -ti:${PORT} | xargs kill`);
console.error('');
console.error(' 2. Change the port in config.json:');
console.error(' Edit config.json and modify the "port" field');
console.error(`${'='.repeat(80)}\n`);
process.exit(1);
} else {
logError('Failed to start server', err);
process.exit(1);
}
});
} catch (error) {
logError('Failed to start server', error);
process.exit(1);
}
})();