Flutter Web, Node.js, CORS and Cookies

Leonard Arnold
The Startup
Published in
5 min readJun 30, 2020

Hi everybody. Let’s be honest straight to the beginning: I didn’t know anything about CORS before I stumbled on it with flutter web. I am a mobile developer and all the javascript full stack work I did was already preconfigured by some developers who definitely knew what they were doing. I guess we all are web developer now, which is a cool thing but here I tell you my story with flutter web, CORS and cookies and how I approached it to get my work done.

We need to have a solid debug setup

My goal was to develop and debug my flutter web app in my chrome browser, at the same time I wanted to debug my nodejs application which was running on a different port. It’s straight forward to do so, but it’s not if cookies are involved. Cookies magically aren’t stored and resent with every request and it took me a while to track down why. Important to say is, it works when nodejs serves the flutter web app, so they have the same origin. For production it’s already fine and working right now, but for developing it’s not the way to go since we want to enjoy all debugging features for web also!

First step: Avoid CORS exceptions

CORS means Cross-Origin Resource Sharing. It’s a security mechanism built into the browser which kicks in if the website and the server are from different origins. Different ports on localhost are different origins. So if you try to call an api with flutter web which is hosted on the same machine but with a different port you will get a CORS error and you will be stopped immediately.

Access to XMLHttpRequest at ‘http://localhost:3000/auth/signin' from origin ‘http://localhost:8200' has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

I have read about several ways to avoid CORS issues:

  1. Setup a proxy — Ok, first of all I think it’s logical. I am working with a team of developers which use different operation systems. I do not want that everybody needs to configure a proxy just to debug our flutter web app.
  2. Starting the browser with CORS disabled — Not good for me. Same argument as above. If you start a browser without CORS I don’t know which other side effects it will have and as I said we all have different operation systems.
  3. Allow Cross Origin from the server — This is what the article will cover. Important here is that you have control over the server to explicitly allow the cross origin requests.

Allow CORS from a nodejs express setup is simple using the package cors:

var app = express();
var cors = require('cors');
app.use(cors({
origin: ["http://localhost:8200", "http://127.0.0.1:8200"],
credentials: true,
}));

The most important detail here is that you have to explicitly tell the web app’s domain — a wild card is not enough if you want to use cookies! Set credentials: true and for origin put the url you are using for your flutter web app.

Usually flutter will run the web app every time on a different port. You can (and you should, so you don’t need to change the nodejs config before every start) set a fixed port for it with the additional command line arguments:

flutter run -d chrome --web-hostname=127.0.0.1 --web-port=8200

It seems done right? Okay let’s run it and see what we got.

The sign in cookie was set. Both of the CORS headers were set.

Headers are correctly set. Server sends the sign in cookie. This seems to work. But if we look further, the cookie is not stored, so it’s not been sent for the next requests! We are not authenticated!

Applications cookies are not set!

Step Two: Make the cookie work

So we are now able to make requests, they work, the server responds. It sends back the right cookie but it can’t be used to authenticate further requests. Let’s remember that all this already works if flutter web and the nodejs application have the same origin. It’s just not working on the local machine running it on different ports.

I use the http package to sign in and for further requests:

import 'package:http/http.dart' as http;
...
http.Response response = await http
.post("$host/$url", body: data, headers: headers)
.timeout(reqTimeoutDuration);

Digging into the http source code I found a interesting piece of code:

/// Whether to send credentials such as cookies or authorization headers for  
/// cross-site requests.
///
/// Defaults to `false`.
bool withCredentials = false;

The variable withCredentials is false. I set it manually to true, flutter clean and voilà:

Cookie is set. It works despite Cross Origin Requests.

I dislike doing things manually over and over again, so I wrote a little script for it:

#!/bin/bash
echo ""
if [[ $# == 0 ]]; then
find ~/Library/flutter/.pub-cache/hosted/pub.dartlang.org -name "browser_client.dart" -type f -exec gsed -i 's/bool withCredentials = false/bool withCredentials = true/g' {} \;
echo "http package browser client patched!"
elif [[ $1 == "revert" ]]; then
find ~/Library/flutter/.pub-cache/hosted/pub.dartlang.org -name "browser_client.dart" -type f -exec gsed -i 's/bool withCredentials = true/bool withCredentials = false/g' {} \;
echo "http package browser client patch reverted!"
else
echo "use this patch without cmd line argument or with \"revert\""
echo "e.g.: $ sh patch_local_cookie_cors_restriction.sh revert"
fi
echo ""
exit 0

This is tested on a mac. Notice gsed on linux is just sed and you may have to change it to you flutter SDK directory.

Wrapping it up

CORS + cookies + flutter web need a special treatment if you want to debug it easily on your local machine. Avoiding CORS is not enough. We need to change the http package to make it work. I prefer this way than setting up a proxy or a browser where CORS is disabled. Now I can debug as usual without having to compile flutter web first and serve it with node (as I do for production). The downside is that we patch the http package on our local machine and we don’t want that for production or even the hosted dev environment. Using a CI for deploying the apps is safe to patch the package on you local machine, as it is the only place where you have to do it.

--

--