Koa集成GraphQL时nodemon重启报3000端口占用如何修复
问题现象
给KoaJS服务接入GraphQL能力时,nodemon监听到文件变更触发服务重启,控制台持续抛出端口占用错误:
Error: listen EADDRINUSE: address already in use :::3000 at Server.setupListenHandle [as _listen2] (node:net:1334:16) at listenInCluster (node:net:1382:12) ... code: 'EADDRINUSE', errno: -48, syscall: 'listen', address: '::', port: 3000 } [nodemon] app crashed - waiting for file changes before starting...
原有问题代码如下:
import { ApolloServer, gql } from "apollo-server-koa"; import { ApolloServerPluginDrainHttpServer } from "apollo-server-core"; import Koa from "koa"; import http from "http"; const typeDefs = gql` type Query { hello: String } `; const resolvers = { Query: { hello: () => "Hello world!", }, }; async function startApolloServer() { const httpServer = http.createServer(); const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, cache: "bounded", plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], }); await server.start(); const app = new Koa(); server.applyMiddleware({ app, path: "/graphql" }); httpServer.on("request", app.callback()); await new Promise<void>((resolve) => httpServer.listen({ port: 3000 }, resolve) ); console.log(`🚀 Server ready at http://localhost:3000${server.graphqlPath}`); return { server, app }; } startApolloServer()
问题原因
报错核心是旧服务进程没有被正常终止:nodemon触发重启时会给原有Node进程发送终止信号,但代码里没有配置优雅关停逻辑,旧进程持有的3000端口没有被主动释放,新进程启动时尝试监听同个端口就会触发地址占用错误。
另外原代码中HTTP服务绑定Koa回调的写法不规范,也可能造成请求句柄泄漏,拖慢端口释放速度。
修复方法
- 修正HTTP服务初始化逻辑,创建服务时直接传入Koa实例的回调,移除单独绑定request事件的写法
- 新增进程终止信号监听,收到nodemon发送的
SIGINT/SIGTERM信号时,主动关停Apollo服务与HTTP服务,确保端口被及时释放
修复后的完整代码:
import { ApolloServer, gql } from "apollo-server-koa"; import { ApolloServerPluginDrainHttpServer } from "apollo-server-core"; import Koa from "koa"; import http from "http"; const typeDefs = gql` type Query { hello: String } `; const resolvers = { Query: { hello: () => "Hello world!", }, }; async function startApolloServer() { const app = new Koa(); const httpServer = http.createServer(app.callback()); const server = new ApolloServer({ typeDefs, resolvers, csrfPrevention: true, cache: "bounded", plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], }); await server.start(); server.applyMiddleware({ app, path: "/graphql" }); await new Promise<void>((resolve) => httpServer.listen({ port: 3000 }, resolve) ); console.log(`🚀 Server ready at http://localhost:3000${server.graphqlPath}`); // 优雅关停逻辑 const shutdown = async () => { await server.stop(); httpServer.close(() => { process.exit(0); }); }; process.on('SIGINT', shutdown); process.on('SIGTERM', shutdown); return { server, app, httpServer }; } startApolloServer()
临时排查时如果端口已经被残留僵尸进程占用,可以执行对应命令手动释放端口后再重启服务:
- Mac/Linux 系统:执行
lsof -ti:3000 | xargs kill -9 - Windows 系统(PowerShell):执行
Stop-Process -Id (Get-NetTCPConnection -LocalPort 3000).OwningProcess -Force
内容的提问来源于stack exchange,提问作者retr0327
相关产品推荐
相关产品推荐

