【问题标题】:ERR_STREAM_WRITE_AFTER_END when executing query with MySQL2 running on Node.js使用在 Node.js 上运行的 MySQL2 执行查询时出现 ERR_STREAM_WRITE_AFTER_END
【发布时间】:2020-06-26 09:06:31
【问题描述】:

我正在实现一项功能,该功能应在单击按钮时通过向后端发送请求并使用 MySQL2 更新数据库来延长用户会话的生命周期。

为此,我编写了以下前端代码:

onClose: function (oAction) {
    try {
        if (oAction == "YES") {
            let reqURL = "/sessionExtend";
            let reqData = {
                session_id: sessionStorage.getItem("SessionId"),
                user_id: sessionStorage.getItem("UserId")
            };
            let callbackOK = function (responseData) {
                curr.onSuccessfulResponse(curr, responseData, "sessionExtendSuccess", "sessionExtendFail", "", false);
            };
            let callbackErr = function (responseData) {
                curr.onErrorResponse(curr, responseData, "sessionExtendFail");
            };

            curr.performRequest(reqURL, reqData, callbackOK, callbackErr);
        }
    } catch (err) {
        console.log(err);
        MessageToast.show(sMsg);
    }
}

请求被app.js接收,它使用MySQL2建立数据库连接并将请求转发给DAL:

app.post("/sessionExtend", async function (req, res) {

    let session_id = req.body.session_id;
    let user_id = req.body.user_id;

    let con = DAL.getConnection();

    res.setHeader("Content-Type", "application/json");

    try {

        const response = await DAL.sessionExtend(con, session_id, user_id);

        res.send(JSON.stringify({
            "result": true,
            "message": "session extended"
        }));

    } catch (e) {

        res.send(JSON.stringify({
            "result": false,
            "message": "can not extend session"
        }));

    }

    con.close();

});

DAL 模块执行 SQL 查询并应返回成功或错误的结果:

sessionExtend: async function sessionExtend(con, session_id, user_id) {

    con.connect(function (err) {
        try {
            if (err) throw err;
            con.query(qryDict.SQL_QUERIES.setUpdateExtendSession, [session_id, user_id], function (err) {

                let result;

                if (err) {
                    result = JSON.stringify({
                        "result": false,
                        "message": "failure"
                    });
                } else {
                    result = JSON.stringify({
                        "result": true,
                        "message": "success"
                    });
                }

                return result;

            });
        } catch (err) {
            let result = JSON.stringify({
                "result": false,
                "message": err
            });

            return result;
        }
    });
},

问题是当我在调试器中执行这段代码时,我得到一个异常:

ERR_STREAM_WRITE_AFTER_END 错误 [ERR_STREAM_WRITE_AFTER_END]:写入 结束后 在 Socket.Writable.write (_stream_writable.js:297:11) 在 Connection.write (C:\Users\User\IdeaProjects\TST\node_modules\mysql2\lib\connection.js:226:17) 在 Connection.writePacket (C:\Users\User\IdeaProjects\TST\node_modules\mysql2\lib\connection.js:271:12) 在 ClientHandshake.sendCredentials (C:\Users\User\IdeaProjects\TST\node_modules\mysql2\lib\commands\client_handshake.js:64:16) 在 ClientHandshake.handshakeInit (C:\Users\User\IdeaProjects\TST\node_modules\mysql2\lib\commands\client_handshake.js:137:12) 在 ClientHandshake.execute (C:\Users\User\IdeaProjects\TST\node_modules\mysql2\lib\commands\command.js:39:22) 在 Connection.handlePacket (C:\Users\User\IdeaProjects\TST\node_modules\mysql2\lib\connection.js:417:32) 在 PacketParser.onPacket (C:\Users\User\IdeaProjects\TST\node_modules\mysql2\lib\connection.js:75:12) 在 PacketParser.executeStart (C:\Users\User\IdeaProjects\TST\node_modules\mysql2\lib\packet_parser.js:75:16) 在套接字。 (C:\Users\User\IdeaProjects\TST\node_modules\mysql2\lib\connection.js:82:25)

我还注意到,在调试过程中,我首先在前端得到来自后端的响应,然后才到达 DAL 中的断点 con.query(qryDict.SQL_QUERIES.setUpdateExtendSession, [session_id, user_id], function (err) {…}

我的问题:

  1. 为什么我会得到ERR_STREAM_WRITE_AFTER_END 以及如何避免它?

  2. 为什么我首先在​​前端得到来自后端的响应,然后才到达 DAL 中的断点?我假设await DAL.sessionExtend(con, session_id, user_id) 应该等到 DAL 上的任务完成并且承诺将得到解决。

【问题讨论】:

  • 您不是在等待您的 con.connect 和 con.query 调用。如果您的 SQL 库有一个 Promise 接口,请使用该接口并 await 它。否则使用require('util').promisify 将其变成一个。
  • 流结束后的写入发生是因为您在 con.query 甚至运行之前执行了 con.close。
  • @CherryDT,我假设与DAL.getConnection() 类似,它包装了 MySQL2 的createConnection(…),我会检查require('util').promisify。关于con.query(…),我用了一个回调,应该是查询完成就可以到达。
  • 是的,但是您的 sessionExtend 函数早已在回调被执行时返回。请参阅下面的答案
  • 糟糕,我现在才看到您指定使用的是 MySQL2。编辑我的答案。

标签: javascript node.js async-await data-access-layer mysql2


【解决方案1】:

CherryDT 的帮助下,通过切换到 ES7 async/await-wrapper 版本的 MySQL2 — mysql2/promise 解决了问题。

为了节省其他公众的时间,最终的即用型代码:

app.js

app.post("/sessionExtend", async function (req, res) {

    let session_id = req.body.session_id;
    let user_id = req.body.user_id;

    const con = await DAL.getConnection();

    res.setHeader("Content-Type", "application/json");

    const response = await DAL.sessionExtend(con, session_id, user_id);

    res.send(JSON.stringify({
        "result": response.result,
        "message": response.message
    }));

    await con.close();

});

DAL.js

sessionExtend: async function sessionExtend(con, session_id, user_id) {

    let result;

    const [rows, fields] = await con.execute(qryDict.SQL_QUERIES.setUpdateExtendSession, [session_id, user_id]);

    if (rows.warningStatus === 0) {
        result = {
            "result": true,
            "message": "session extended"
        };
    } else {
        result = {
            "result": false,
            "message": "session is not extended"
        };
    }

    return result;

},

如您所见,现在代码更易于理解和维护。

附:我的建议:使用async/await,它们很棒,尽量避免回调。

【讨论】:

    【解决方案2】:

    简而言之:你不是在等待con.connectioncon.query,所以外部代码继续调用con.close并返回前端结果,稍后con.query尝试通过现在关闭的连接发送查询,导致此异常。

    您正在编写异步函数,但您只将它们设置为“半异步”。

    例如,这是行不通的:

    async function getStuff () {
      stuff.get(function (err, data) {
        if (err) throw err // kills your process if it happend!
        return data.stuff // returns to nowhere
      })
    }
    
    // later on:
    const stuff = await getStuff()
    console.log(stuff) // prints undefined!
    

    ...因为本质上,您的异步函数只是同步调用另一个函数(不等待它)然后立即返回任何内容(即undefined):

    async function getStuff () {
      stuff.get(...)
      // as you can see, no return inside getStuff
    }
    

    稍后,您传递的回调将运行,但是您的外部代码的火车已经离开了平台。

    您想要做的是让stuff.get 返回一个承诺(大多数现代库都会这样做,即使它们另外公开了一个回调承诺以与旧代码库兼容)和await 它:

    async function getStuff () {
      const data = await stuff.get() // waits for the stuff to come back
      return data.stuff // actually returns the stuff
      // The `if (err) throw err` now became unnecessary as well
    }
    
    // later on:
    const stuff = await getStuff()
    console.log(stuff) // prints the stuff!
    

    如果您的 SQL 库将公开一个 Promise 接口,您可以简单地 await 它。您写道您正在使用mysql2。如果需要require('mysql2/promise'),这个库有一个promise 接口。我建议切换到promise接口而不是回调接口!

    还有一种方法可以将现有的con 连接“升级”到 promise 接口:con.promise()。所以你只需使用con = DAL.getConnection().promise() 而不是con = DAL.getConnection()

    然后,您可以像这样重写代码(或等效代码,具体取决于您选择的库):

    async function sessionExtend(con, session_id, user_id) {
        try {
            await con.connect()
            await con.query(qryDict.SQL_QUERIES.setUpdateExtendSession, [session_id, user_id])
            return JSON.stringify({ result: true, message: 'success' })
        } catch (err) {
            return JSON.stringify({ result: false, message: err.toString() })
        }
    }
    

    编辑: 以下部分实际上已过时,因为mysql2 允许将现有连接升级到承诺接口,但无论如何我都会把它留在这里,以防它帮助其他人类似的情况!

    如果你不能切换到 promise 接口,你可以改为 promisify 现有的调用(不过看起来有点复杂):

    const { promisify } = require('util')
    
    async function sessionExtend(con, session_id, user_id) {
        try {
            await promisify(con.connect).call(con)
            await promisify(con.query).call(con, qryDict.SQL_QUERIES.setUpdateExtendSession, [session_id, user_id])
            return JSON.stringify({ result: true, message: 'success' })
        } catch (err) {
            return JSON.stringify({ result: false, message: err.toString() })
        }
    }
    

    util.promisify 包装了一个期望(err, data) 回调的函数,将其转换为一个返回promise 的异步函数。由于con.query 等。虽然是con 上的方法,但它们需要保留该上下文,这就是我写promisify(con.query).call(con, ...) 而不仅仅是promisify(con.query)(...) 的原因。

    【讨论】:

    • 感谢您的详细解释。我正在使用MySQL2,在他们的连接建立代码 sn-ps 中没有提到承诺是强制性的。
    • 他们不是。但是,如果您不使用它们,则必须始终使用回调,而您没有这样做(这就是代码不起作用的原因)。如果你这样做了,代码会起作用,但它也会变得更加复杂。这就像用信用卡支付订阅费用可能并不总是强制,但它非常有帮助,因为否则你每个月都会遇到用现金支付的麻烦。 ;)
    • 我更新了我的答案以反映 MySQL2 中存在的选项
    • 非常感谢您的详细解释并指出mysql2/promise,这确实可以摆脱回调地狱并保持代码简洁明了。
    猜你喜欢
    • 2019-12-02
    • 1970-01-01
    • 2014-04-21
    • 1970-01-01
    • 2023-03-05
    • 2019-03-04
    • 2013-11-11
    • 2011-06-26
    • 2014-10-31
    相关资源
    最近更新 更多