Express.js+ts-node API运行数小时后出现context deadline exceeded超时错误求助
Express API部署后几小时出现请求超时(context deadline exceeded)问题排查与修复
问题描述
我使用Express.js + ts-node构建API,通过NGINX与Docker部署。服务启动后30分钟至1小时内运行正常,但数小时后,所有来自外部应用及Postman的请求均返回错误:context deadline exceeded (Client.Timeout exceeded while awaiting headers)。
主API模块代码
import express, { Request, Response } from "express"; import cors from "cors"; import dotenv from "dotenv"; import path from 'path'; import * as http from 'http'; import { CheckerRequestBody, CheckerResponse } from "./types"; import { blacklistHandler } from "../src/accounts/accountsMonitor"; import helmet from "helmet"; import { asyncLogger } from "../config/appConfig"; import { AuthToken, BlacklistedWallet } from "../db/models/entity"; import { createAuthorizationMiddleware } from "./middleware"; dotenv.config({ path: path.resolve(__dirname, '../.env') }); const app = express(); const PORT = parseInt(process.env.PORT) || 3000; const DEFAULT_API_PATH: string = "/api/v1/"; export class DataFromDB{ public static authTokens: AuthToken[] = []; public static blacklistedAddresses: Set<string> = new Set<string>(); } app.use(express.json()); app.use(cors({ origin: '*', methods: ['GET', 'POST'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: false, })); app.use(helmet()); app.use(DEFAULT_API_PATH, createAuthorizationMiddleware()); app.post(DEFAULT_API_PATH + 'check', async (req: Request<{}, {}, CheckerRequestBody>, res: Response<CheckerResponse>) => { try { const body: CheckerRequestBody = req.body; const accountAddresses: string[] = body.accountAddresses; const result = accountAddresses.some(address => DataFromDB.blacklistedAddresses.has(address)); if (result) { res.status(200).json({ blacklisted: true, message: "Blacklisted wallet found" }); } else { res.status(200).json({ blacklisted: false, message: "Blacklisted wallet not found" }); } } catch (error) { res.status(500).json({ blacklisted: false, message: "Error on server occurred" }); asyncLogger.error(`Error on API occurred: ${error}`); } }); async function getDataFromDatabase(){ try{ const blacklistedAddressesObjects: BlacklistedWallet[] = await blacklistHandler.getAllEntities(blacklistHandler.BlacklistedWalletRepository); DataFromDB.authTokens = await blacklistHandler.getAllEntities(blacklistHandler.AuthTokensRepository); DataFromDB.blacklistedAddresses = new Set(blacklistedAddressesObjects.map(({ accountAddress }) => accountAddress)); asyncLogger.info(`Succesfully retrieved data from Database in API module.`); } catch(error){ asyncLogger.error(`Error occurred while retrieving data from Database in API: ${error}`) } } const server = http.createServer(app); setInterval(async () => { await getDataFromDatabase(); }, 1000); server.listen(PORT, async () => { asyncLogger.info('Server is currently running on port 3000.'); }); server.on('error', (error) => { asyncLogger.error(`Error occurred: ${error.message}`); });
中间件代码
import { asyncLogger } from "../config/appConfig"; import { Request, Response, NextFunction } from "express"; import { DataFromDB } from "./schema"; export const createAuthorizationMiddleware = () => { return async (req: Request, res: Response, next: NextFunction) => { if (req.path.startsWith("/docs")){ next(); } const authHeader = req.get('Authorization'); if (!authHeader) { res.status(403).json({ blacklisted: false, message: "No Authentication token provided" }); return; } try { const foundToken = DataFromDB.authTokens.find((entry) => entry.token === authHeader); if (!foundToken) { res.status(403).json({ blacklisted: false, message: "Invalid Authentication token" }); return; } const currentDate = new Date(); const expirationDate = new Date(foundToken.expirationDate); if (currentDate > expirationDate) { res.status(403).json({ blacklisted: false, message: "Authentication token has expired" }); return; } next(); } catch (error) { asyncLogger.error(`Error reading the auth tokens: ${error}`); res.status(500).json({ blacklisted: false, message: "Error on server occurred" }); } }; };
核心问题分析
- 高频数据库轮询耗尽连接:代码中用
setInterval每秒执行一次数据库查询,频繁的请求会耗尽数据库连接池资源,导致后续API请求无法获取数据库连接,进而阻塞处理流程,最终引发超时。 - 静态数据更新的竞态条件:
DataFromDB的静态属性被频繁替换,中间件读取数据时可能遇到不完整的更新,导致请求挂起。 - 中间件路径处理漏洞:
/docs路径调用next()后未终止后续逻辑,可能触发不必要的认证检查,引发异常。 - 资源与超时配置缺失:Docker容器资源限制、NGINX超时配置不合理也可能导致请求提前超时。
修复方案
1. 降低数据库轮询频率
将1秒的轮询间隔调整为更合理的时间(比如5分钟,即300000毫秒),如果需要实时同步数据,建议改用数据库变更通知机制(如PostgreSQL的LISTEN/NOTIFY、MongoDB的Change Streams)替代轮询:
setInterval(async () => { await getDataFromDatabase(); }, 300000); // 5分钟一次
2. 原子化更新静态数据
先一次性获取所有数据库数据,再原子替换静态属性,避免竞态条件:
async function getDataFromDatabase(){ try{ // 并行获取数据,提升效率 const [blacklistedAddressesObjects, authTokens] = await Promise.all([ blacklistHandler.getAllEntities(blacklistHandler.BlacklistedWalletRepository), blacklistHandler.getAllEntities(blacklistHandler.AuthTokensRepository) ]); const newBlacklistedSet = new Set(blacklistedAddressesObjects.map(({ accountAddress }) => accountAddress)); // 原子替换,避免中间读取到不完整数据 DataFromDB.authTokens = authTokens; DataFromDB.blacklistedAddresses = newBlacklistedSet; asyncLogger.info(`Succesfully retrieved data from Database in API module.`); } catch(error){ asyncLogger.error(`Error occurred while retrieving data from Database in API: ${error}`) } }
3. 修复中间件路径逻辑
调用next()后添加return,终止后续认证代码执行:
if (req.path.startsWith("/docs")){ next(); return; // 跳过后续认证逻辑 }
4. 配置资源与超时
- Docker:为容器分配足够的内存与CPU资源,避免资源耗尽。
- NGINX:调整代理超时配置,避免NGINX提前断开连接:
location / { proxy_pass http://api; proxy_connect_timeout 60s; proxy_read_timeout 60s; proxy_send_timeout 60s; }
- Express:添加全局请求超时中间件,防止请求长时间挂起:
import timeout from 'connect-timeout'; app.use(timeout('10s')); app.use((req: Request, res: Response, next: NextFunction) => { if (!req.timedout) next(); });
内容的提问来源于stack exchange,提问作者slsforme
相关产品推荐
相关产品推荐

