Search code examples
javascriptnode.jsexpresssessionfetch

Fetch() calls from Node.js always create new Express sessions


Below is a stripped-down segment of a webshop server with 2 APIs: ./login to login and ./products to show products. The products should only be shown after a succesful login.

I'm using TypeScript with Node.js and Express with session management enabled. The code below also tries to test the server by sending fetch() requests to the APIs. NOTE: So I'm sending fetch() requests from within Node.js, not from a browser. The code below produces the following output:

Login session ID: um2RykooJCer7Oy7YmJQs7s9ge3mZWL1
Products session ID: uAk03ochUM56rzJjPvbiRnFuPG8CUqTB
Products response: Error: Not logged in

It can be seen that the session IDs are different, that's why it fails to show products. When I leave the code running and type in the API calls manually in a browser:

http://localhost:3000/login
http://localhost:3000/products

Then the code works correctly, with the browser displaying "Login OK" and then "Apple, Orange, Pear" both with the same session ID.

So the question is: How can the server test be made to work correctly with fetch() from Node.js, the same way as it works when called from a browser? Are there some options that can be given to the fetch() requests or to Express? I have not found any yet.

Bonus question: Can it also be made to work with the npm package node-fetch? I use that because the built-in fetch() from Node.js gives errors in other places.

PS I'm using the most recent versions of Node.js (v21.5), Express, express-session and node-fetch.

import Express from "express";
import Session from "express-session";
// Uncomment this to use the node-fetch module, but gives the same result:
// import fetch from "node-fetch";

declare module 'express-session' {
    interface SessionData
    {
        loggedIn: boolean;
    }
}

class App
{
    express = Express();

    start()
    {
        let session = Session( { secret: "my-secret", /* options? */ } );
        this.express.use( session );
        this.express.get( "/login", ( request, response ) => this.onLoginRequest( request, response ) );
        this.express.get( "/products", ( request, response ) => this.onProductsRequest( request, response ) );
        this.express.listen( 3000, () => this.requestLogin() );
    }

    // =================================== Login

    requestLogin()
    {
        fetch( 'http://localhost:3000/login', { /* options? */ } )
            .then( result => this.showProducts() );
    }

    onLoginRequest( request: Express.Request, response: Express.Response )
    {
        console.log( "Login session ID: " + request.session.id );
        request.session.loggedIn = true;
        response.send( "Login OK" );
    }

    // =================================== Products

    showProducts()
    {
        fetch( 'http://localhost:3000/products', { /* options? */ } )
            .then( result => this.onProducts( result ) );
    }

    onProductsRequest( request: Express.Request, response: Express.Response )
    {
        console.log( "Products session ID: " + request.session.id );
        if( !request.session.loggedIn )
            response.send( "Error: Not logged in" );
        else
            response.send( "Apple, Orange, Pear" );
    }

    onProducts( result: any )
    {
        result.text()
            .then( ( text: string ) => console.log( "Products response: " + text ) );
    }
}

( new App() ).start();

Solution

  • From a browser, add this option to your fetch() call:

    { withCredentials: true }
    

    Without that, fetch() may not send cookies with the request. Without the cookies, there is no connection to an existing session so Express creates a new empty session on each new request.

    If you're using node-fetch() server-side, then you will need some sort of cookie jar that collects and retains cookies from one request to the next. Nodejs, unlike a browser, does not collect and keep track of the cookies for you. There a module node-fetch-cookies that does that for node-fetch code. You can also manually collect a returned cookie off a response and then send that cookie back with the next request (the same way a browser would).