Get and Set a Single Cookie with Node.js HTTP Server

Viewed 378742

I want to be able to set a single cookie, and read that single cookie with each request made to the nodejs server instance. Can it be done in a few lines of code, without the need to pull in a third party lib?

var http = require('http');

http.createServer(function (request, response) {
  response.writeHead(200, {'Content-Type': 'text/plain'});
  response.end('Hello World\n');
}).listen(8124);

console.log('Server running at http://127.0.0.1:8124/');

Just trying to take the above code directly from nodejs.org, and work a cookie into it.

15 Answers

Let me repeat this part of question that answers here are ignoring:

Can it be done in a few lines of code, without the need to pull in a third party lib?


Reading Cookies

Cookies are read from requests with the Cookie header. They only include a name and value. Because of the way paths work, multiple cookies of the same name can be sent. In NodeJS, all Cookies in as one string as they are sent in the Cookie header. You split them with ;. Once you have a cookie, everything to the left of the equals (if present) is the name, and everything after is the value. Some browsers will accept a cookie with no equal sign and presume the name blank. Whitespaces do not count as part of the cookie. Values can also be wrapped in double quotes ("). Values can also contain =. For example, formula=5+3=8 is a valid cookie.

/**
 * @param {string} [cookieString='']
 * @return {[string,string][]} String Tuple
 */
function getEntriesFromCookie(cookieString = '') {
  return cookieString.split(';').map((pair) => {
    const indexOfEquals = pair.indexOf('=');
    let name;
    let value;
    if (indexOfEquals === -1) {
      name = '';
      value = pair.trim();
    } else {
      name = pair.substr(0, indexOfEquals).trim();
      value = pair.substr(indexOfEquals + 1).trim();
    }
    const firstQuote = value.indexOf('"');
    const lastQuote = value.lastIndexOf('"');
    if (firstQuote !== -1 && lastQuote !== -1) {
      value = value.substring(firstQuote + 1, lastQuote);
    }
    return [name, value];
  });
}

const cookieEntries = getEntriesFromCookie(request.headers.Cookie); 
const object = Object.fromEntries(cookieEntries.slice().reverse());

If you're not expecting duplicated names, then you can convert to an object which makes things easier. Then you can access like object.myCookieName to get the value. If you are expecting duplicates, then you want to do iterate through cookieEntries. Browsers feed cookies in descending priority, so reversing ensures the highest priority cookie appears in the object. (The .slice() is to avoid mutation of the array.)


Settings Cookies

"Writing" cookies is done by using the Set-Cookie header in your response. The response.headers['Set-Cookie'] object is actually an array, so you'll be pushing to it. It accepts a string but has more values than just name and value. The hardest part is writing the string, but this can be done in one line.

/**
 * @param {Object} options
 * @param {string} [options.name='']
 * @param {string} [options.value='']
 * @param {Date} [options.expires]
 * @param {number} [options.maxAge]
 * @param {string} [options.domain]
 * @param {string} [options.path]
 * @param {boolean} [options.secure]
 * @param {boolean} [options.httpOnly]
 * @param {'Strict'|'Lax'|'None'} [options.sameSite]
 * @return {string}
 */
function createSetCookie(options) {
  return (`${options.name || ''}=${options.value || ''}`)
    + (options.expires != null ? `; Expires=${options.expires.toUTCString()}` : '')
    + (options.maxAge != null ? `; Max-Age=${options.maxAge}` : '')
    + (options.domain != null ? `; Domain=${options.domain}` : '')
    + (options.path != null ? `; Path=${options.path}` : '')
    + (options.secure ? '; Secure' : '')
    + (options.httpOnly ? '; HttpOnly' : '')
    + (options.sameSite != null ? `; SameSite=${options.sameSite}` : '');
}

const newCookie = createSetCookie({
  name: 'cookieName',
  value: 'cookieValue',
  path:'/',
});
response.headers['Set-Cookie'].push(newCookie);

Remember you can set multiple cookies, because you can actually set multiple Set-Cookie headers in your request. That's why it's an array.


Note on external libraries:

If you decide to use the express, cookie-parser, or cookie, note they have defaults that are non-standard. Cookies parsed are always URI Decoded (percent-decoded). That means if you use a name or value that has any of the following characters: !#$%&'()*+/:<=>?@[]^`{|} they will be handled differently with those libraries. If you're setting cookies, they are encoded with %{HEX}. And if you're reading a cookie you have to decode them.

For example, while email=name@domain.com is a valid cookie, these libraries will encode it as email=name%40domain.com. Decoding can exhibit issues if you are using the % in your cookie. It'll get mangled. For example, your cookie that was: secretagentlevel=50%007and50%006 becomes secretagentlevel=507and506. That's an edge case, but something to note if switching libraries.

Also, on these libraries, cookies are set with a default path=/ which means they are sent on every url request to the host.

If you want to encode or decode these values yourself, you can use encodeURIComponent or decodeURIComponent, respectively.


References:


Additional information:

Using Some ES5/6 Sorcery & RegEx Magic

Here is an option to read the cookies and turn them into an object of Key, Value pairs for client side, could also use it server side.

Note: If there is a = in the value, no worries. If there is an = in the key, trouble in paradise.

More Notes: Some may argue readability so break it down as you like.

I Like Notes: Adding an error handler (try catch) wouldn't hurt.

const iLikeCookies = () => {
    return Object.fromEntries(document.cookie.split('; ').map(v => v.split(/=(.+)/))); 
}

const main = () => {
    // Add Test Cookies
    document.cookie = `name=Cookie Monster;expires=false;domain=localhost`
    document.cookie = `likesCookies=yes=withARandomEquals;expires=false;domain=localhost`;

    // Show the Objects
    console.log(document.cookie)
    console.log('The Object:', iLikeCookies())

    // Get a value from key
    console.log(`Username: ${iLikeCookies().name}`)
    console.log(`Enjoys Cookies: ${iLikeCookies().likesCookies}`)
}

enter image description here

What is going on?

iLikeCookies() will split the cookies by ; (space after ;):

["name=Cookie Monster", "likesCookies=yes=withARandomEquals"]

Then we map that array and split by first occurrence of = using regex capturing parens:

[["name", "Cookie Monster"], ["likesCookies", "yes=withARandomEquals"]]

Then use our friend `Object.fromEntries to make this an object of key, val pairs.

Nooice.

If you don't care what's in the cookie and you just want to use it, try this clean approach using request (a popular node module):

var request = require('request');
var j = request.jar();
var request = request.defaults({jar:j});
request('http://www.google.com', function () {
  request('http://images.google.com', function (error, response, body){
     // this request will will have the cookie which first request received
     // do stuff
  });
});

I wrote this simple function just pass req.headers.cookie and cookie name

const getCookieByName =(cookies,name)=>{
    const arrOfCookies = cookies.split(' ')
    let yourCookie = null

    arrOfCookies.forEach(element => {
        if(element.includes(name)){
            yourCookie = element.replace(name+'=','')
        }
    });
    return yourCookie
}

I know that there are many answer to this question already, but here's a function made in native JS.

function parseCookies(cookieHeader) {
    var cookies = {};
    cookieHeader
        .split(";")
        .map(str => str.replace("=", "\u0000")
        .split("\u0000"))
        .forEach(x => cookies[x[0]] = x[1]); 

    return cookies;
}

It starts by taking in the document.cookie string. Every key-value pair is separated by a semicolon (;). Therefore the first step is to divide the string up each key-value pair.

After that, the function replaces the first instance of "=" with a random character that isn't in the rest of the string, for this function I decided to use the NULL character (\u0000). The key-value pair can now be split into just two pieces. The two pieces can now be combined into JSON.

Related