dio icon indicating copy to clipboard operation
dio copied to clipboard

Flutter Web: Preflight check doesn't send cookie, creates new session (NodeJS backend)

Open cfuj opened this issue 2 years ago • 1 comments

New Issue Checklist

  • [X] I have searched for a similar issue in the project and found none

Issue Info

Info Value
Platform Name e.g. Flutter Web
Platform Version e.g. Flutter 2.10.4 / Chrome 102
Dio Version e.g. 4.06
Android Studio / Xcode Version Android Studio 2022.1.1
Repro rate 100%
Repro with our demo prj e.g. does it happen with our demo project?
Demo project link e.g. link to a demo project that highlights the issue

Issue Description and Steps

Please fill in the detailed description of the issue (full output of any stack trace, compiler error, ...) and the steps to reproduce the issue.

I have posted the same issue to the Flutter issue tracker.

I detail below how I can use an alternative method in the server, so preflight isn't triggering the full request, but then the main-request gives me a different session ID each time. The method I'm demonstrating here shows that the sessions are persisting, and that that Flutter-Web is getting, and sometimes sending the cookie.

Steps to Reproduce

  1. I've stripped the demo server (Node JS) down to what I think is the bare minimum
import express from "express";

const app: express.Application = module.exports = express(),
  http = require("http"),
  session = require('express-session');

app.use(session(
  {
    secret: 'demo',
    resave: true,
    saveUninitialized: true,
    cookie: {
      path: '/',
      crossDomain: true,
      httpOnly: true,
      secure: false,
      maxAge: 1000000000,
    },
    rolling: true,
  }));

app.use('/', (req, res, next) => {
  res.header({
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Origin': req.headers.origin,
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    'Access-Control-Allow-Credentials': 'true'
  });
  console.log(req.method, req.session.id);
  res.end();
});

const server = http.createServer({
  rejectUnauthorized: false,
  host: '0.0.0.0'
}, app).listen(3000, function () {
  console.log(`Demo Server started.`);
});
  1. I make a request in Dio like this.
Dio dio = Dio();
dio.options.baseUrl = baseURL;
dio.options.connectTimeout = 5000;
dio.options.extra['withCredentials'] = true;
Map<String, String> sendHeaders = {};
sendHeaders['Content-Type'] = 'application/json';
    await dio.post(url, options: Options(headers: sendHeaders));

Expected results:

Both the preflight and the request will trigger the app.use('/'), so I expect to see...

Request 1 within same browser-session
  OPTIONS [sessionID]
  POST [same session ID]
Request 2 within same browser-session
  OPTIONS [same sessionID]
  POST [same session ID]
Request 3 within same browser-session
  OPTIONS [same sessionID]
  POST [same session ID]

Actual results:

Request 1 within same browser-session
  OPTIONS [sessionID#1]
  POST [session ID#2]
Request 2 within same browser-session
  OPTIONS [sessionID#3]
  POST [same session ID#2]
Request 3 within same browser-session
  OPTIONS [sessionID#4]
  POST [same session ID#2]

Notes

NodeJS has a utility called CORS which I can use instead of the above app.use, and that stops this duplicate output. I'm deliberately taking advantage of it here.

If I do that, and the browser only sends one request, then that main request does not send the cookie, and each session is different, making maintaining login state impossible.

This is frustrating, because this is easy in Android/IOS/etc. You catch the cookie on the first request and send it with subsequent requests.

image

cfuj avatar Jun 01 '22 13:06 cfuj

My temporary solution is this in the server

  res.header('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Credentials', 'true');
  console.log(req.method, req.session.id)

  if (req.method == "GET" || req.method == "POST") {
    return next();
  } else {
    req.session.destroy(() => {});
    res.end();
  }

It seems like this is an issue only present in Debug build. Release build only sends one request (I'm not getting any OPTIONS calls from the above console.log). And all session IDs for the same session are the same.

It still absolutely makes debugging a pain, the steps involved aren't very obvious.

cfuj avatar Jun 01 '22 19:06 cfuj