Tedious: Requests can only be made in the LoggedIn state, not the SentClientRequest state (code: 'EINVALIDSTATE')

Created on 28 Sep 2016  ·  23Comments  ·  Source: tediousjs/tedious

In my example below, I'm trying to serially execute an array of functions. But the Requests can only be made in the LoggedIn state, so I had to check for that connection.state !== connection.STATE.LOGGED_IN. And as you can see in the Read function below had to put the request back in queue in order to get rid of the error.

{ [RequestError: Requests can only be made in the LoggedIn state, not the SentClientRequest state]
  message: 'Requests can only be made in the LoggedIn state, not the SentClientRequest state',
  code: 'EINVALIDSTATE' }

Is there a better way of achieving this? I see that a similar issues were brought up before #19 and #355. Is there a recommended way of executing multiple requests on a connection?

Code:

var Connection = require('tedious').Connection;  
var Request = require('tedious').Request;  
var TYPES = require('tedious').TYPES;  
var async = require('async');

// Create connection to database
var config = {
        userName: 'mylogin',  
        password: 'mypassword',  
        server: 'localhost',
        options: {
            // encrypt: true, /*If you are connecting to a Microsoft Azure SQL database, you will need this*/
            database: 'testdb'
        }
    }
var connection = new Connection(config);  

// Attempt to connect and execute queries if connection goes through
connection.on('connect', function(err) {  
    if (err) {
        console.log(err);
    }  
    else    {
        console.log("Connected");

        // Execute all functions in the array serially
        async.waterfall([
            function Start(callback){
                console.log("Starting...");
                callback(null, 'Jake', 'United States');
            },
            function Insert(name, location, callback){
                console.log("Inserting into Table...");

                request = new Request("INSERT dbo.employees (name, location) OUTPUT INSERTED.id VALUES (@Name, @Location);", function(err){  
                     if (err) {
                            console.log(err);
                        }  
                    }
                );  
                request.addParameter('Name', TYPES.NVarChar, name);  
                request.addParameter('Location', TYPES.NVarChar, location);  

                request.on('row', function(columns) {  
                    columns.forEach(function(column) {  
                      if (column.value === null) {  
                        console.log('NULL');  
                      } else {  
                        console.log("Employee id inserted is " + column.value);  
                      }  
                    });  
                }); 

                // Check how many rows were inserted
                request.on('doneInProc', function(rowCount, more) {  
                    console.log(rowCount + ' row(s) inserted');  
                    callback(null, 'Jared');
                });             

                connection.execSql(request);  
            },
            function Read(callback){
                // Requests can only be made in the LoggedIn state, so check for that
                if (connection.state !== connection.STATE.LOGGED_IN) {
                    // Put the request back on the dispatcher if connection is not in LoggedIn state
                    setTimeout(Read, 0, callback);
                    return;
                }

                console.log("Reading rows from the Table...");

                // Create the request to read from table
                request = new Request("SELECT * FROM dbo.employees;", function(err) {  
                    if (err) {
                        console.log(err);
                    }  
                });  

                // Output the number of rows read 
                request.on('doneInProc', function (rowCount, more, rows) {  
                    console.log(rowCount + ' row(s) returned');  
                    callback(null);
                });  

                connection.execSql(request);  
            }],
            function Complete(err, result) {
                if(err) {
                    console.log("Error:", err);
                }
                else {
                    console.log("Done!");
                }
            }
        )
    }
});

Most helpful comment

For people coming here from Google (like I did): tedious-connection-pool offers a great way of combatting this problem. Using pool.acquire((err, connection) => ...) and connection.release() is an easy way to prevent concurrency issues. If you don't want multiple connections, configure the pool with {min: 1, max: 1} to effectively serialize queries.

All 23 comments

There is a 'callback' property you can set on Request and there is a 'requestCompleted' event on Request, both seem work. And those are the two things we didn't try :)

There may be some cleanup and documentation needed on this.

Try these and see if it works.

Using 'callback':

  request.callback = function (err, rowCount, rows) {
      // rows is not being set but rowCount is. May be a bug.
      if (err) {
        // Error handling.
      } else {
        // Next SQL statement.
      }
  };

Using 'requestCompleted':

  request.on('requestCompleted', function () {
      // Next SQL statement.
  });

I'll actually put this notes in https://github.com/tediousjs/tedious/issues/61 and close this one as it's the same issue.

Re-opening the issue. https://github.com/tediousjs/tedious/issues/61 is entirely unrelated. The issues referred to by @SaloniSonpal are actually closed. So reopening this for tracking.

This needs some cleanup and documentation.

Thanks @tvrprasad. Both these worked, and are way cleaner. callback approach does give the advantage of handling the error. Event: 'requestCompleted' definitely needs to be documented.

(Sending from my phone, excuse the terseness).

Setting the callback property will overwrite the callback that you passed
to the Request constructor. Instead, you should call the callback from
async inside the callback passed to The Request constructor. This way you
can also forward errors correctly to async.

Ah, missed that callback is set to the callback passed in. So the right way would be:

  request = new Request(sqlQuery, function (err, rowCount, rows) {
    if (err) {
      // Error handling.
    } else {
      // Next SQL statement.
    }
  });

@arthurschreiber Any reason for the 'requestCompleted' event? Seems redundant. Another thing I notice is rows parameter passed in to the callback is always empty. Is there a scenario where it'll have valid data? I'll update documentation.

Another thing I notice is rows parameter passed in to the callback is always empty. Is there a scenario where it'll have valid data? I'll update documentation.

There is the rowCollectionOnRequestCompletion option that can be specified when connecting. Having it as a global option is a bit weird, I think it would make more sense to have this per-request.

The main idea behind this is the following: If the rowCollectionOnRequestCompletion option is enabled rows are stored in memory until the corresponding Request is completed (and currently, I think they are even kept until the Request gets gc'ed). With small result sets, that's usually not an issue, but with large result sets, that can negatively impact performance a lot. The preferred approach is to just use the rows callback, and if you need to collect all rows into an array, you can do so yourself.

I don't think this is optimal, having this configurable per-request instead of per-connection would probably be better - but the Request constructor does not take any options yet, so the method signature would've to be changed.

Any reason for the 'requestCompleted' event?

I'm not sure - this was added before I started working on tedious.

Opened #459 to track the rowCollectionOnRequestCompletion issue. I'm sure we can add and handle that
in a backward compatible way.

Setting the callback property will overwrite the callback that you passed
to the Request constructor. Instead, you should call the callback from
async inside the callback passed to The Request constructor. This way you
can also forward errors correctly to async.

@arthurschreiber You're right. That is the right way to not override the results/ errors. Works like a charm :-)

Created #464 to improve documentation on Event: 'requestCompleted'

For people coming here from Google (like I did): tedious-connection-pool offers a great way of combatting this problem. Using pool.acquire((err, connection) => ...) and connection.release() is an easy way to prevent concurrency issues. If you don't want multiple connections, configure the pool with {min: 1, max: 1} to effectively serialize queries.

Anyone please suggest, windows authentication mode is not working for nodejs2Sql connectivity.

@wafaabbass tedious doesn't support windows integrated authentication yet. We are working on it, you can track it in #415.

Hey, everyone, I have the same problem.

debug:

Requests can only be made in the LoggedIn state, not the SentClientRequest state
(node:5180) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): RequestError: Requests can only be made in the LoggedIn state, not the SentClientRequest state
(node:5180) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Is it resolved yet? This is my code.

function createAsPromiseWithTransaction(callback, data, connection) {

  const transactionReady = (err) => {
    if (err) {
      return callback(err)
    }
    return callback(null)
  }

  const transactionFailed = (originalErr) =>
    (err) => {
      if (err) {
        return callback(err)
      }
      return callback(originalErr)
    }

  const willIGetArea = new Promise(
    (resolve, reject) => {
      const create = function(data, connection) {
        let areaIdInserted = null
        const request = new Request(CONSTANTS.SPNames.Area,
          (err, rowCount, rows) => err ? reject(err) : resolve(areaIdInserted))

        request.addParameter('IUser', TYPES.UniqueIdentifier, data.iUser)
        request.addParameter('ParentArea', TYPES.Int, data.parentArea)
        request.addParameter('Name', TYPES.VarChar, data.nameArea)
        request.addParameter('ShortName', TYPES.VarChar, data.shortNameArea)
        request.addParameter('Description', TYPES.VarChar, data.descriptionArea)
        request.addParameter('Gender', TYPES.Bit, data.gender)
        request.addParameter('Colour', TYPES.VarChar, data.colour)
        request.addParameter('X', TYPES.Int, data.x)
        request.addParameter('Y', TYPES.Int, data.y)
        request.addParameter('Active', TYPES.Int, data.active)

        request.on('row', function(columns) {
          areaIdInserted = columns[1].value
        });

        connection.callProcedure(request)
      }

      const transactionStarted = function(err) {
        if (err) return callback(err)
        create(data, connection)
      }

      connection.beginTransaction(transactionStarted)
    })

  const willIGetSymptom = function(areaId) {

    return new Promise(function(resolve, reject) {

      const create = function(data, connection, areaId) {
        let symptomIdInserted = null
        const request = new Request(CONSTANTS.SPNames.Symptom,
          (err, rowCount, rows) => err ? reject(err) : resolve(symptomIdInserted))

        request.addParameter('IUser', TYPES.UniqueIdentifier, data.iUser)
        request.addParameter('AreaId', TYPES.BigInt, areaId)
        request.addParameter('Code', TYPES.VarChar, data.code)
        request.addParameter('Name', TYPES.VarChar, data.nameSymptom)
        request.addParameter('ShortName', TYPES.VarChar, data.shortNameSymptom)
        request.addParameter('Description', TYPES.VarChar, data.descriptionSymptom)
        request.addParameter('Gender', TYPES.Bit, data.gender)
        request.addParameter('Active', TYPES.Bit, data.active)

        request.on('row', function(columns) {
          symptomIdInserted = columns[1].value
        });


        connection.callProcedure(request)
      }

      create(data, connection, areaId)

    })
  }

  const willIGetModifier = new Promise(function(resolve, reject) {

    const request = new Request(
      CONSTANTS.SPNames.Modifier,
      function(err, rowCount, rows) {
        if (err) {
          reject(err)
        } else {
          resolve(rowCount,rows)
        }
      })

    request.addParameter('IUser', TYPES.UniqueIdentifier, data.iUser)
    request.addParameter('Name', TYPES.VarChar, data.nameModifier)
    request.addParameter('Order', TYPES.SmallInt, data.order)
    request.addParameter('Active', TYPES.Bit, data.active)

    connection.callProcedure(request)

  })

  const runPromise = () =>
    willIGetArea
    .then( (areaId) =>
      willIGetSymptom(areaId)
      .then( (symptomId) =>
        willIGetModifier
        .then((rowCount,rows) => connection.commitTransaction(transactionReady))
        .catch((err) => connection.rollbackTransaction(transactionFailed(err)))
      )
      .catch((err) => connection.rollbackTransaction(transactionFailed(err)))
    )it 
    .catch((err) => connection.rollbackTransaction(transactionFailed(err)))

  runPromise()

}

If I call willIGetArea and willIGetSymptom, all this works but, if I call it with willGetModifier, I have the same problem as well. Thanks a lot.

In case someone finds it helpful, since Google returns this page first for this error-message.
It worked for my single-connection-based service to make a Promise-based wrapper for the connection. Not sure it is a good solution in general though, for I'm not an experienced js-developer:

class PromisedConnection {
    constructor(config) {
        // this.connection always points to a promise resolving after the last assigned request
        // initial setting is either resolved once a new connection is established or rejected if an error occurs
        this.connection = new Promise((resolve, reject) => {
            const dbConnection = new Connection(config);
            dbConnection.on("connect", function (err) {
                if (err) {
                    reject(err);
                }
                else {
                    resolve(dbConnection);
                }
            });
        });
    }

    execute(request) {
        const nextConnection = new Promise((resolve, reject) => {
            // after scheduling new request this.connection should be reassigned to be the last in promise queue
            this.connection
                .catch( (reason) => {
                    reject(reason);
                } )
                .then( (dbConnection) => { // a new request can be executed only within the connection is free again (resolved after the last request)
                    request.on("requestCompleted", () => { // add an additional event listener in order to release connection after the request is done
                        resolve(dbConnection);
                    });
                    dbConnection.execSql(request);
                });
        } );
        this.connection = nextConnection;
    }
}

And the query itself can be executed as this:

const dbConnection = new PromisedConnection(config);

function getObjList(query, parameters = []) {
    return new Promise((resolve, reject) => {
        const objList = [];
        let request = new Request(
            query,
            function (err, rowCount, rows) {
                if (err) {
                    reject(err);
                } else if (rowCount < 1) {
                    reject(new Error("0 rows returned from DB"));
                }
            }
        );
        for (const {name, type, value} of parameters) {
            request.addParameter(name, type, value);
        };

        request.on("row", (columns) => {
            objList.push(Obj.fromColumns(columns)); // here I just make a specific object from each row
        });

        request.on("requestCompleted", () => {
            resolve(objList);
        });

        dbConnection.execute(request);
    });
}

So getObjList now can be executed any time without checking the connection state.

Concern here is well. In my code I do a connection request, a sql request, and then a insert request. It errors on insert request unless i do another connection request between sql and insert request even after i wait for the requestCompleted after the sql request.

A common reason for this error is that _only one query can be executed on a connection at a time_. You need to wait until the request callback is executed, either with an error or with the result before making another request. Please visit the FAQ page here

Since there seems to be too many various issues on this forum. The error Requests can only be made in the LoggedIn state, not the SentClientRequest state can be caused by a variety of different reasons. If you experience this error and you did not find the FAQ page helpful, please raise your own issue with the following information so that we may address your issue with more precision:

  • Tedious Version
  • Your configuration for Connection
  • A reproducible script

Thanks! 😄

Was able to resolve this, problem was on my side, didn't realize you need a separate connection for each request

@mgarf What was your solution? I also didn't realize I need a separate connection for each request. I'm having concurrency issues like the others in this discussion.

@mikebutak you don't necessarily need a separate connection for each request. You just need one connection handling one request _at a time_. To handle multiple request on one connection, you need to execute a new request in the callback of the previous one, after it's finished. For example,

function executeStatement() {
  request = new Request("select 42, 'hello world'", function(err, rowCount) {
    if (err) {
      console.log(err);
    } else {
      console.log(rowCount + ' rows');
    }

    connection.execSql(new Request("select 42, 'hello world'", function(err, rowCount) {
      if (err) {
        console.log(err);
      } else {
        console.log(rowCount + ' rows');
      }
      connection.close();
    }))
  });
  connection.execSql(request);

I am posting a full solution using callbacks for this issue. I had the same problem and when looking at google i ended up here. I load tested this code with a script executing 1 http call every second and also half a second with no issues.

const {Connection, Request} = require("tedious");

const executeSQL = (sql, callback) => {
  let connection = new Connection({
    "authentication": {
      "options": {
        "userName": "USERNAME",
        "password": "PASSWORD"
      },
      "type": "default"
    },
    "server": "SERVER",
    "options": {
      "validateBulkLoadParameters": false,
      "rowCollectionOnRequestCompletion": true,
      "database": "DATABASE",
      "encrypt": true
    }
  });

  connection.connect((err) => {
    if (err)
      return callback(err, null);

    const request = new Request(sql, (err, rowCount, rows) => {
      connection.close();

      if (err)
        return callback(err, null);

      callback(null, {rowCount, rows});
    });

    connection.execSql(request);
  });
};

executeSQL("SELECT * FROM users", (err, data) => {
  if (err)
    console.error(err);

  console.log(data.rowCount);
});

//or

executeSQL("SELECT * FROM users", (err, {rowCount, rows}) => {
  if (err)
    console.error(err);

  console.log(rowCount);
});

I am posting a full solution using callbacks for this issue. I had the same problem and when looking at google i ended up here. I load tested this code with a script executing 1 http call every second and also half a second with no issues.

const {Connection, Request} = require("tedious");

const executeSQL = (sql, callback) => {
  let connection = new Connection({
    "authentication": {
      "options": {
        "userName": "USERNAME",
        "password": "PASSWORD"
      },
      "type": "default"
    },
    "server": "SERVER",
    "options": {
      "validateBulkLoadParameters": false,
      "rowCollectionOnRequestCompletion": true,
      "database": "DATABASE",
      "encrypt": true
    }
  });

  connection.connect((err) => {
    if (err)
      return callback(err, null);

    const request = new Request(sql, (err, rowCount, rows) => {
      connection.close();

      if (err)
        return callback(err, null);

      callback(null, {rowCount, rows});
    });

    connection.execSql(request);
  });
};

executeSQL("SELECT * FROM users", (err, data) => {
  if (err)
    console.error(err);

  console.log(data.rowCount);
});

//or

executeSQL("SELECT * FROM users", (err, {rowCount, rows}) => {
  if (err)
    console.error(err);

  console.log(rowCount);
});

This solution works but is it a valid implementation? How feasible is that, considering there might be hundreds of thousands of inserts/selects and creating a connection for each of them? Thanks for the solution btw, I am just trying to understand if it is a viable solution. Especially if we are using an Azure Function?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

SaloniSonpal picture SaloniSonpal  ·  5Comments

gladmustang picture gladmustang  ·  6Comments

ghost picture ghost  ·  5Comments

tvrprasad picture tvrprasad  ·  5Comments

arthurschreiber picture arthurschreiber  ·  8Comments