Node.js中getRequestData Promise在createBook函数中无法resolve的问题
getRequestData()导致Promise无法resolve,服务无响应 我实现了返回Promise的getRequestData()函数,用于解析API请求的请求体数据。该函数在createUser()中调用正常,但在createBook()中调用时,Promise始终无法resolve,对应API接口无响应。
排查后发现:/book POST路由会先调用authenticateUser()做身份验证,而这个函数内部已经调用过一次getRequestData()消费了请求流。由于Node.js的HTTP请求流是一次性可读流,后续createBook()再次调用getRequestData()时,无法再读取到数据,导致Promise无法触发end事件完成resolve。
相关代码实现
getRequestData函数
async function getRequestData(req, res) { return new Promise((resolve, reject) => { const data = []; req .on("data", (chunk) => { data.push(chunk); }) .on("end", () => { const dataDecoded = Buffer.concat(data).toString(); const parsedData = JSON.parse(dataDecoded); resolve(parsedData); }) .on("error", (error) => { reject(error); }); }); }
createUser函数
async function createUser(req, res) { try { const userData = await getRequestData(req, res); const allRegisteredUsers = await getUsersFromDb(); const users = parseUsersData(allRegisteredUsers) const userExists = users.find((user) => { return user.username === userData.username; }); if (userExists) { return res.end("User already exists!"); } users.push(userData); await writeUsersToDb(users); res.end(JSON.stringify({ message: "User created successfully", user: userData })) } catch (error) { console.log(error); res.statusCode = 400; return res.end("Error creating a new user!"); } }
createBook函数
async function createBook(req, res) { try { const newBook = await getRequestData(req, res); console.log(newBook); res.end("Create new book"); } catch (error) { res.writeHead(500) res.end(error); } }
服务器请求处理函数
async function serverListener(req, res) { try { res.setHeader("Content-Type", "application/json"); if (req.url === "/user/create" && req.method === "POST") { await createUser(req, res); } else if (req.url === "/users") { await authenticateUser(req, res, ["admin"]) getAllUsers(req, res); } else if (req.url === "/book" && req.method === "POST") { await authenticateUser(req, res, ["admin"]) createBook(req, res); } else if (req.url === "/book" && req.method === "PATCH") { await authenticateUser(req, res, ["admin"]) updateBook(req, res); } else if (req.url === "/book" && req.method === "DELETE") { await authenticateUser(req, res, ["admin"]) deleteBook(req, res); } else if (req.url === "/book/loan" && req.method === "POST") { loanOutBook(req, res); } else if (req.url === "/book/return" && req.method === "POST") { returnLoanedBook(req, res); } else { res.statusCode = 404; res.end("The route does not exists.") } } catch(err) { console.log(err); res.statusCode = 500; res.end(err); }; }
authenticateUser函数
function authenticateUser(req, res, roles) { return new Promise(async (resolve, reject) => { try { const receivedData = await getRequestData(req, res); const userLoginData = receivedData.userLogin; if (!userLoginData) { return reject("You need to be authenticated to continue"); } const allRegisteredUsers = await getUsersFromDb(); const users = parseUsersData(allRegisteredUsers); const userFound = users.find((user) => { return ( user.username === userLoginData.username && user.password === userLoginData.password ); }); if (userFound && roles.includes(userFound.role)) { resolve(userFound); } else if (userFound && !roles.includes(userFound.role)) { res.statusCode = 401; reject( "You don't have the required permission to perform this operation." ); } else { res.statusCode = 404; reject("Your user account doesn't exist! Create a new user."); } } catch (error) { reject(error); } }); }
解决方案
核心思路是只读取一次请求流,并将解析后的数据缓存起来,后续所有需要使用请求体的函数直接读取缓存数据,避免重复消费流。
方案1:修改getRequestData()实现缓存逻辑
修改函数,将解析后的请求体数据缓存到req对象上,后续调用直接返回缓存数据:
async function getRequestData(req, res) { // 若已解析过请求体,直接返回缓存数据 if (req.parsedData) { return req.parsedData; } return new Promise((resolve, reject) => { const data = []; req .on("data", (chunk) => { data.push(chunk); }) .on("end", () => { try { const dataDecoded = Buffer.concat(data).toString(); const parsedData = JSON.parse(dataDecoded); // 将解析结果缓存到req对象 req.parsedData = parsedData; resolve(parsedData); } catch (parseError) { reject(parseError); } }) .on("error", (error) => { reject(error); }); }); }
此方案无需修改其他函数的调用逻辑,authenticateUser()和createBook()仍可正常调用getRequestData(),第二次及以后调用都会直接返回缓存的解析结果。
方案2:在路由处理前统一解析请求体
在serverListener中,对需要读取请求体的请求方法(POST/PATCH/DELETE)统一解析一次,后续所有函数直接从req.parsedData读取数据:
async function serverListener(req, res) { try { res.setHeader("Content-Type", "application/json"); // 统一解析请求体,仅执行一次 if (["POST", "PATCH", "DELETE"].includes(req.method)) { req.parsedData = await getRequestData(req, res); } // 后续路由逻辑不变 if (req.url === "/user/create" && req.method === "POST") { await createUser(req, res); } else if (req.url === "/users") { await authenticateUser(req, res, ["admin"]) getAllUsers(req, res); } // ... 其他路由逻辑 } catch(err) { console.log(err); res.statusCode = 500; res.end(err); }; }
同时修改authenticateUser()和createBook(),直接读取缓存数据:
// 修改authenticateUser function authenticateUser(req, res, roles) { return new Promise(async (resolve, reject) => { try { const receivedData = req.parsedData; // 直接用缓存数据 // ... 后续逻辑不变 } catch (error) { reject(error); } }); } // 修改createBook async function createBook(req, res) { try { const newBook = req.parsedData; // 直接用缓存数据 console.log(newBook); res.end("Create new book"); } catch (error) { res.writeHead(500) res.end(error); } }
问题本质
Node.js的HTTP请求流是可读流,这类流只能被消费一次:一旦data和end事件被触发并处理完毕,再次监听这些事件不会产生任何响应。因此第二次调用getRequestData()时,Promise会一直等待end事件,永远无法resolve,最终导致服务超时无响应。
内容的提问来源于stack exchange,提问作者Emmanuel Oloyede

