mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-22 16:43:35 +00:00
fix(memory): comprehensive req closure capture fixes
Additional fixes for memory leaks: - Bedrock stream: extract _apiKeyIdBedrock, _rateLimitInfoBedrock, _requestBodyBedrock - Non-stream requests: extract variables at block start - Non-stream service calls: use extracted variables - Non-stream usage recording: use extracted variables All async callbacks now use local variables instead of req.* references, preventing the entire request object (including large req.body with images) from being retained by closures. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -607,6 +607,11 @@ async function handleMessagesRequest(req, res) {
|
|||||||
)
|
)
|
||||||
} else if (accountType === 'bedrock') {
|
} else if (accountType === 'bedrock') {
|
||||||
// Bedrock账号使用Bedrock转发服务
|
// Bedrock账号使用Bedrock转发服务
|
||||||
|
// 🧹 内存优化:提取需要的值
|
||||||
|
const _apiKeyIdBedrock = req.apiKey.id
|
||||||
|
const _rateLimitInfoBedrock = req.rateLimitInfo
|
||||||
|
const _requestBodyBedrock = req.body
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const bedrockAccountResult = await bedrockAccountService.getAccount(accountId)
|
const bedrockAccountResult = await bedrockAccountService.getAccount(accountId)
|
||||||
if (!bedrockAccountResult.success) {
|
if (!bedrockAccountResult.success) {
|
||||||
@@ -614,7 +619,7 @@ async function handleMessagesRequest(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await bedrockRelayService.handleStreamRequest(
|
const result = await bedrockRelayService.handleStreamRequest(
|
||||||
req.body,
|
_requestBodyBedrock,
|
||||||
bedrockAccountResult.data,
|
bedrockAccountResult.data,
|
||||||
res
|
res
|
||||||
)
|
)
|
||||||
@@ -625,13 +630,13 @@ async function handleMessagesRequest(req, res) {
|
|||||||
const outputTokens = result.usage.output_tokens || 0
|
const outputTokens = result.usage.output_tokens || 0
|
||||||
|
|
||||||
apiKeyService
|
apiKeyService
|
||||||
.recordUsage(req.apiKey.id, inputTokens, outputTokens, 0, 0, result.model, accountId)
|
.recordUsage(_apiKeyIdBedrock, inputTokens, outputTokens, 0, 0, result.model, accountId)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error('❌ Failed to record Bedrock stream usage:', error)
|
logger.error('❌ Failed to record Bedrock stream usage:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
queueRateLimitUpdate(
|
queueRateLimitUpdate(
|
||||||
req.rateLimitInfo,
|
_rateLimitInfoBedrock,
|
||||||
{
|
{
|
||||||
inputTokens,
|
inputTokens,
|
||||||
outputTokens,
|
outputTokens,
|
||||||
@@ -758,18 +763,26 @@ async function handleMessagesRequest(req, res) {
|
|||||||
}
|
}
|
||||||
}, 1000) // 1秒后检查
|
}, 1000) // 1秒后检查
|
||||||
} else {
|
} else {
|
||||||
|
// 🧹 内存优化:提取需要的值,避免后续回调捕获整个 req
|
||||||
|
const _apiKeyIdNonStream = req.apiKey.id
|
||||||
|
const _apiKeyNameNonStream = req.apiKey.name
|
||||||
|
const _rateLimitInfoNonStream = req.rateLimitInfo
|
||||||
|
const _requestBodyNonStream = req.body
|
||||||
|
const _apiKeyNonStream = req.apiKey
|
||||||
|
const _headersNonStream = req.headers
|
||||||
|
|
||||||
// 🔍 检查客户端连接是否仍然有效(可能在并发排队等待期间断开)
|
// 🔍 检查客户端连接是否仍然有效(可能在并发排队等待期间断开)
|
||||||
if (res.destroyed || res.socket?.destroyed || res.writableEnded) {
|
if (res.destroyed || res.socket?.destroyed || res.writableEnded) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`⚠️ Client disconnected before non-stream request could start for key: ${req.apiKey?.name || 'unknown'}`
|
`⚠️ Client disconnected before non-stream request could start for key: ${_apiKeyNameNonStream || 'unknown'}`
|
||||||
)
|
)
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非流式响应 - 只使用官方真实usage数据
|
// 非流式响应 - 只使用官方真实usage数据
|
||||||
logger.info('📄 Starting non-streaming request', {
|
logger.info('📄 Starting non-streaming request', {
|
||||||
apiKeyId: req.apiKey.id,
|
apiKeyId: _apiKeyIdNonStream,
|
||||||
apiKeyName: req.apiKey.name
|
apiKeyName: _apiKeyNameNonStream
|
||||||
})
|
})
|
||||||
|
|
||||||
// 📊 监听 socket 事件以追踪连接状态变化
|
// 📊 监听 socket 事件以追踪连接状态变化
|
||||||
@@ -940,11 +953,11 @@ async function handleMessagesRequest(req, res) {
|
|||||||
? await claudeAccountService.getAccount(accountId)
|
? await claudeAccountService.getAccount(accountId)
|
||||||
: await claudeConsoleAccountService.getAccount(accountId)
|
: await claudeConsoleAccountService.getAccount(accountId)
|
||||||
|
|
||||||
if (account?.interceptWarmup === 'true' && isWarmupRequest(req.body)) {
|
if (account?.interceptWarmup === 'true' && isWarmupRequest(_requestBodyNonStream)) {
|
||||||
logger.api(
|
logger.api(
|
||||||
`🔥 Warmup request intercepted (non-stream) for account: ${account.name} (${accountId})`
|
`🔥 Warmup request intercepted (non-stream) for account: ${account.name} (${accountId})`
|
||||||
)
|
)
|
||||||
return res.json(buildMockWarmupResponse(req.body.model))
|
return res.json(buildMockWarmupResponse(_requestBodyNonStream.model))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -957,11 +970,11 @@ async function handleMessagesRequest(req, res) {
|
|||||||
if (accountType === 'claude-official') {
|
if (accountType === 'claude-official') {
|
||||||
// 官方Claude账号使用原有的转发服务
|
// 官方Claude账号使用原有的转发服务
|
||||||
response = await claudeRelayService.relayRequest(
|
response = await claudeRelayService.relayRequest(
|
||||||
req.body,
|
_requestBodyNonStream,
|
||||||
req.apiKey,
|
_apiKeyNonStream,
|
||||||
req,
|
req, // clientRequest 用于断开检测,保留但服务层已优化
|
||||||
res,
|
res,
|
||||||
req.headers
|
_headersNonStream
|
||||||
)
|
)
|
||||||
} else if (accountType === 'claude-console') {
|
} else if (accountType === 'claude-console') {
|
||||||
// Claude Console账号使用Console转发服务
|
// Claude Console账号使用Console转发服务
|
||||||
@@ -969,11 +982,11 @@ async function handleMessagesRequest(req, res) {
|
|||||||
`[DEBUG] Calling claudeConsoleRelayService.relayRequest with accountId: ${accountId}`
|
`[DEBUG] Calling claudeConsoleRelayService.relayRequest with accountId: ${accountId}`
|
||||||
)
|
)
|
||||||
response = await claudeConsoleRelayService.relayRequest(
|
response = await claudeConsoleRelayService.relayRequest(
|
||||||
req.body,
|
_requestBodyNonStream,
|
||||||
req.apiKey,
|
_apiKeyNonStream,
|
||||||
req,
|
req, // clientRequest 保留用于断开检测
|
||||||
res,
|
res,
|
||||||
req.headers,
|
_headersNonStream,
|
||||||
accountId
|
accountId
|
||||||
)
|
)
|
||||||
} else if (accountType === 'bedrock') {
|
} else if (accountType === 'bedrock') {
|
||||||
@@ -985,9 +998,9 @@ async function handleMessagesRequest(req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = await bedrockRelayService.handleNonStreamRequest(
|
const result = await bedrockRelayService.handleNonStreamRequest(
|
||||||
req.body,
|
_requestBodyNonStream,
|
||||||
bedrockAccountResult.data,
|
bedrockAccountResult.data,
|
||||||
req.headers
|
_headersNonStream
|
||||||
)
|
)
|
||||||
|
|
||||||
// 构建标准响应格式
|
// 构建标准响应格式
|
||||||
@@ -1017,11 +1030,11 @@ async function handleMessagesRequest(req, res) {
|
|||||||
// CCR账号使用CCR转发服务
|
// CCR账号使用CCR转发服务
|
||||||
logger.debug(`[DEBUG] Calling ccrRelayService.relayRequest with accountId: ${accountId}`)
|
logger.debug(`[DEBUG] Calling ccrRelayService.relayRequest with accountId: ${accountId}`)
|
||||||
response = await ccrRelayService.relayRequest(
|
response = await ccrRelayService.relayRequest(
|
||||||
req.body,
|
_requestBodyNonStream,
|
||||||
req.apiKey,
|
_apiKeyNonStream,
|
||||||
req,
|
req, // clientRequest 保留用于断开检测
|
||||||
res,
|
res,
|
||||||
req.headers,
|
_headersNonStream,
|
||||||
accountId
|
accountId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1070,14 +1083,14 @@ async function handleMessagesRequest(req, res) {
|
|||||||
const cacheCreateTokens = jsonData.usage.cache_creation_input_tokens || 0
|
const cacheCreateTokens = jsonData.usage.cache_creation_input_tokens || 0
|
||||||
const cacheReadTokens = jsonData.usage.cache_read_input_tokens || 0
|
const cacheReadTokens = jsonData.usage.cache_read_input_tokens || 0
|
||||||
// Parse the model to remove vendor prefix if present (e.g., "ccr,gemini-2.5-pro" -> "gemini-2.5-pro")
|
// Parse the model to remove vendor prefix if present (e.g., "ccr,gemini-2.5-pro" -> "gemini-2.5-pro")
|
||||||
const rawModel = jsonData.model || req.body.model || 'unknown'
|
const rawModel = jsonData.model || _requestBodyNonStream.model || 'unknown'
|
||||||
const { baseModel: usageBaseModel } = parseVendorPrefixedModel(rawModel)
|
const { baseModel: usageBaseModel } = parseVendorPrefixedModel(rawModel)
|
||||||
const model = usageBaseModel || rawModel
|
const model = usageBaseModel || rawModel
|
||||||
|
|
||||||
// 记录真实的token使用量(包含模型信息和所有4种token以及账户ID)
|
// 记录真实的token使用量(包含模型信息和所有4种token以及账户ID)
|
||||||
const { accountId: responseAccountId } = response
|
const { accountId: responseAccountId } = response
|
||||||
await apiKeyService.recordUsage(
|
await apiKeyService.recordUsage(
|
||||||
req.apiKey.id,
|
_apiKeyIdNonStream,
|
||||||
inputTokens,
|
inputTokens,
|
||||||
outputTokens,
|
outputTokens,
|
||||||
cacheCreateTokens,
|
cacheCreateTokens,
|
||||||
@@ -1087,7 +1100,7 @@ async function handleMessagesRequest(req, res) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
await queueRateLimitUpdate(
|
await queueRateLimitUpdate(
|
||||||
req.rateLimitInfo,
|
_rateLimitInfoNonStream,
|
||||||
{
|
{
|
||||||
inputTokens,
|
inputTokens,
|
||||||
outputTokens,
|
outputTokens,
|
||||||
|
|||||||
Reference in New Issue
Block a user