46

I'd like to make an http request to a remote server while properly handling cookies (eg. storing cookies sent by the server, and sending those cookies when I make subsequent requests). It'd be nice to preserve any and all cookies

for http request I am using

static Future<Map> postData(Map data) async {
  http.Response res = await http.post(url, body: data); // post api call
  Map data = JSON.decode(res.body);
  return data;
}
Shashi kumar S
  • 873
  • 3
  • 10
  • 21

8 Answers8

76

Here's an example of how to grab a session cookie and return it on subsequent requests. You could easily adapt it to return multiple cookies. Make a Session class and route all your GETs and POSTs through it.

import 'package:http/http.dart' as http;

    class Session {
      Map<String, String> headers = {};
    
      Future<Map> get(String url) async {
        http.Response response = await http.get(url, headers: headers);
        updateCookie(response);
        return json.decode(response.body);
      }
    
      Future<Map> post(String url, dynamic data) async {
        http.Response response = await http.post(url, body: data, headers: headers);
        updateCookie(response);
        return json.decode(response.body);
      }
    
      void updateCookie(http.Response response) {
        String? rawCookie = response.headers['set-cookie'];
        if (rawCookie != null) {
          int index = rawCookie.indexOf(';');
          headers['cookie'] =
              (index == -1) ? rawCookie : rawCookie.substring(0, index);
        }
      }
    }
Vonarian
  • 184
  • 2
  • 11
Richard Heap
  • 48,344
  • 9
  • 130
  • 112
  • Just Question how you'll call it in a widget. – ilovejavaAJ Apr 05 '19 at 22:50
  • thantks for that, working great ! I used this solution, I just changed the class as a Singleton so I can acces one instance in my widgets – TheDrev Jun 04 '19 at 07:05
  • why do you get only the first substring before ' ; ' of the cookie? for example, in this cookie: "OCENTRIC_COOKIE=1; path=/;Secure;HttpOnly", what is after ' ; ' is not important? – alexpfx Jun 29 '19 at 16:17
  • The bits after the semi are instructions to you regarding when you should return the cookie. For example, it may only be relevant to certain path subsets. The instructions are not part of the cookie and should not be sent back to the server. – Richard Heap Jun 29 '19 at 18:20
  • Was wondering why 'Set-Cookie' didn't work, it was actually 'set-cookie'. Postman and even the backend code uses 'Set-Cookie', is there a reason why it is lower case in dart/flutter? – DanT29 Feb 01 '20 at 18:04
  • 2
    Per the RFC all headers are case insensitive, though many servers are non-compliant. For ease of comparison, Dart forces them all to lower case. – Richard Heap Feb 01 '20 at 21:02
  • What is that "index" bit at the end for? Can't I just do `headers['cookie'] = response.headers['set-cookie']`? – daraul Dec 23 '21 at 16:19
  • 1
    @daraul it's to chop off the bit after the semicolon (if present) - see the discussion here: https://stackoverflow.com/questions/50299253/flutter-http-maintain-php-session/50299669#50299669 – Richard Heap Dec 23 '21 at 17:56
  • Hey, @RichardHeap can you please look into it https://stackoverflow.com/questions/73133740/401-unauthorized-error-in-flutter-but-works-fine-in-postman – Sandeep Sharma Jul 27 '22 at 22:15
21

I have improved the Richard Heap's solution to be capable to process multiple 'Set-cookies' and multiple cookies.

In my case, the server returns multiples 'Set-cookies'. The http package concatenate all the set-cookies headers in one header and separate it by comma (',').

class NetworkService {

  final JsonDecoder _decoder = new JsonDecoder();
  final JsonEncoder _encoder = new JsonEncoder();

  Map<String, String> headers = {"content-type": "text/json"};
  Map<String, String> cookies = {};

  void _updateCookie(http.Response response) {
    String allSetCookie = response.headers['set-cookie'];

    if (allSetCookie != null) {

      var setCookies = allSetCookie.split(',');

      for (var setCookie in setCookies) {
        var cookies = setCookie.split(';');

        for (var cookie in cookies) {
          _setCookie(cookie);
        }
      }

      headers['cookie'] = _generateCookieHeader();
    }
  }

  void _setCookie(String rawCookie) {
    if (rawCookie.length > 0) {
      var keyValue = rawCookie.split('=');
      if (keyValue.length == 2) {
        var key = keyValue[0].trim();
        var value = keyValue[1];

        // ignore keys that aren't cookies
        if (key == 'path' || key == 'expires')
          return;

        this.cookies[key] = value;
      }
    }
  }

  String _generateCookieHeader() {
    String cookie = "";

    for (var key in cookies.keys) {
      if (cookie.length > 0)
        cookie += ";";
      cookie += key + "=" + cookies[key];
    }

    return cookie;
  }

  Future<dynamic> get(String url) {
    return http.get(url, headers: headers).then((http.Response response) {
      final String res = response.body;
      final int statusCode = response.statusCode;

      _updateCookie(response);

      if (statusCode < 200 || statusCode > 400 || json == null) {
        throw new Exception("Error while fetching data");
      }
      return _decoder.convert(res);
    });
  }

  Future<dynamic> post(String url, {body, encoding}) {
    return http
        .post(url, body: _encoder.convert(body), headers: headers, encoding: encoding)
        .then((http.Response response) {
      final String res = response.body;
      final int statusCode = response.statusCode;

      _updateCookie(response);

      if (statusCode < 200 || statusCode > 400 || json == null) {
        throw new Exception("Error while fetching data");
      }
      return _decoder.convert(res);
    });
  }
}
Iarly
  • 336
  • 2
  • 3
  • Thanks, your answer helped me to retrive multiple cookies – Manish Dhruw Mar 22 '19 at 06:42
  • 2
    Thanks this helped me. There is a slight bug while you split the rawCookie on '='; it doesn't work when there are empty strings in the split. Following fix helped me. rawCookie.split('=').where((s) => s.isNotEmpty).toList(growable: false) – Piyush Jajoo Jun 29 '19 at 18:08
  • 2
    Also ignore the cases while comparing path and expires - if (key.toLowerCase() != 'path' && key.toLowerCase() != 'expires') { return {key: value}; } – Piyush Jajoo Jun 29 '19 at 18:13
  • The answer does not take into account when there is any '=' in the value part of the cookie. I have to change _setCookie method: if (rawCookie.length > 0) { int idx = rawCookie.indexOf("="); if (idx >= 0) { var key = rawCookie.substring(0, idx).trim(); var value = rawCookie.substring(idx+1).trim(); if (key == 'path' || key == 'expires' || key == 'domain' || key == 'SameSite') return; cookies[key] = value; } } – georkings Nov 27 '20 at 05:27
16

I've published a small flutter library called requests to assist with cookie-aware http requests.

dependencies:
  requests: ^3.0.1

Usage:

import 'package:requests/requests.dart';

// ...

// this will persist cookies
var r1 = await Requests.post("https://example.com/api/v1/login", json: {"username":"...", "password":"..."} ); 
r1.raiseForStatus();

// this will re-use the persisted cookies
var r2 = await Requests.get("https://example.com/api/v1/stuff"); 
r2.raiseForStatus();
print(r2.json()['id'])

find out more about requests

Jossef Harush Kadouri
  • 32,361
  • 10
  • 130
  • 129
  • Thanks a lot sir! Your package worked perfectly for me! – Kartik Mar 14 '19 at 19:06
  • Hi, I am using graphql server and flutter_graphql package from here https://github.com/zino-app/graphql-flutter. Any idea on how I can use this package – Nux Apr 12 '20 at 19:50
  • 1
    @Jossef hi, is there a way to send a second request without terminating first request? I'm having an issue dealing with a cookie that has expire time "session" – CharukaHS Jul 15 '20 at 21:54
  • @CharukaHS please open an issue -> https://github.com/jossef/requests/issues add your request response detailed example – Jossef Harush Kadouri Jul 16 '20 at 07:26
  • seems to have old dependencies can't use this – RanH Jun 16 '21 at 11:11
  • @JossefHarushKadouri will this store the cookie in local storage? Say I run both the post and the get requests mentioned here and close the app and run only the get request (without making the post request), will Requests.get be able to use the previous cookie? If not , what is to be done save the cookie locally ? – saket Jun 06 '22 at 09:06
  • Hey @JossefHarushKadouri Can you please look into it https://stackoverflow.com/questions/73133740/401-unauthorized-error-in-flutter-but-works-fine-in-postman – Sandeep Sharma Jul 27 '22 at 22:15
4

I migrated larly's answer to nullsafety. Also added 'put' function.

import 'dart:convert';
import 'package:http/http.dart' as http;


class NetworkService {
  final JsonDecoder _decoder = const JsonDecoder();
  final JsonEncoder _encoder = const JsonEncoder();

  Map<String, String> headers = {"content-type": "application/json"};
  Map<String, String> cookies = {};

  void _updateCookie(http.Response response) {
    String? allSetCookie = response.headers['set-cookie'];

    if (allSetCookie != null) {
      var setCookies = allSetCookie.split(',');

      for (var setCookie in setCookies) {
        var cookies = setCookie.split(';');

        for (var cookie in cookies) {
          _setCookie(cookie);
        }
      }

      headers['cookie'] = _generateCookieHeader();
    }
  }

  void _setCookie(String? rawCookie) {
    if (rawCookie != null) {
      var keyValue = rawCookie.split('=');
      if (keyValue.length == 2) {
        var key = keyValue[0].trim();
        var value = keyValue[1];

        // ignore keys that aren't cookies
        if (key == 'path' || key == 'expires') return;

        cookies[key] = value;
      }
    }
  }

  String _generateCookieHeader() {
    String cookie = "";

    for (var key in cookies.keys) {
      if (cookie.isNotEmpty) cookie += ";";
      cookie += key + "=" + cookies[key]!;
    }

    return cookie;
  }

  Future<dynamic> get(String url) {
    return http.get(Uri.parse(url), headers: headers).then((http.Response response) {
      final String res = response.body;
      final int statusCode = response.statusCode;

      _updateCookie(response);

      if (statusCode < 200 || statusCode > 400) {
        throw Exception("Error while fetching data");
      }
      return _decoder.convert(res);
    });
  }

  Future<dynamic> post(String url, {body, encoding}) {
    return http.post(Uri.parse(url), body: _encoder.convert(body), headers: headers, encoding: encoding).then((http.Response response) {
      final String res = response.body;
      final int statusCode = response.statusCode;

      _updateCookie(response);

      if (statusCode < 200 || statusCode > 400) {
        throw Exception("Error while fetching data");
      }
      return _decoder.convert(res);
    });
  }

  Future<dynamic> put(String url, {body, encoding}) {
    return http.put(Uri.parse(url), body: _encoder.convert(body), headers: headers, encoding: encoding).then((http.Response response) {
      final String res = response.body;
      final int statusCode = response.statusCode;

      _updateCookie(response);

      if (statusCode < 200 || statusCode > 400) {
        throw Exception("Error while fetching data");
      }
      return _decoder.convert(res);
    });
  }
}
Berkekbgz
  • 41
  • 2
2

If it wasn't a problem, I use dio and cookiejar.

Just add these dependencies in your pubspec.yaml:

dependencies:
  dio: ^4.0.4
  dio_cookie_manager: ^2.0.0
  cookie_jar: ^3.0.1

Here's an example to use it. Make sure you run it as a flutter script, and not a dart script.

import 'package:cookie_jar/cookie_jar.dart';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';


void main() async {
  var dio =  Dio(BaseOptions(
      connectTimeout: 10000,  // in ms
      receiveTimeout: 10000,
      sendTimeout: 10000,
      responseType: ResponseType.plain,
      followRedirects: false,
      validateStatus: (status) { return true; }
  ));   // some dio configurations

  dio.interceptors.add(CookieManager(CookieJar()));

  var firstResponse = await dio.get(
      "https://somewebsite.com/get_login_info");
  print(firstResponse.data);

  var loginResponse = await dio.post(
      "https://somewebsite.com/login",
      data: FormData.fromMap(
          {
            'username': 'YourUsername',
            'password': 'YourPassword',
          }
      ));  // cookies are automatically saved
  print(loginResponse.statusCode);

  var nextResponse = await dio.get(
      "https://somewebsite.com/get_login_info");
  print(nextResponse.data);
}

an example output:

{"is_logged_in": 0}
302
{"is_logged_in": 1}
Hzzkygcs
  • 1,532
  • 2
  • 16
  • 24
1

I found best solution to handle cookies for redirect also

import 'dart:convert';
import 'dart:io';

class CustomHTTPClient{
  final HttpClient _client = new HttpClient();
  Map<String, String> _cookies = Map();

  CustomHTTPClient(){
    _client.connectionTimeout = Duration(seconds: 10);
  }

  Future<String> get(String url, {int maxRedirect = 3}) async {
    final parsedUrl = Uri.parse(url);
    return await _client.getUrl(parsedUrl)
        .then((HttpClientRequest request) {
      request.followRedirects = false;
      _beforeRequest(request);
      return request.close();
    }).then((HttpClientResponse response) async {
      _afterResponse(response);
      if(response.isRedirect && maxRedirect > 0){
        return await response.drain().then((value) => get(parsedUrl.resolve(response.headers.value('location')).toString(), maxRedirect: maxRedirect - 1));
      }
      return response.transform(utf8.decoder).join();
    }).catchError((error, stack){
      print(error);print(stack);
    });
  }

  void _beforeRequest(HttpClientRequest request){
    request.headers.set(HttpHeaders.acceptEncodingHeader, 'gzip, deflate, br');

    // Set cookie
    final String rawCookies = _cookies.keys.map((String name) => '$name=${_cookies[name]}').join('; ');
    if(rawCookies.isNotEmpty) request.headers.set(HttpHeaders.cookieHeader, rawCookies);
  }

  void _afterResponse(HttpClientResponse response){
    response.headers.forEach((String name, List<String> values){
      if(name == 'set-cookie'){ // Get cookies for next request
        values.forEach((String rawCookie){
          try{
            Cookie cookie = Cookie.fromSetCookieValue(rawCookie);
            _cookies[cookie.name] = cookie.value;
          } catch(e){
            final List<String> cookie = rawCookie.split(';')[0].split('=');
            _cookies[cookie[0]] = cookie[1];
          }
        });
        return false;
      }
    });
  }
}
jay padaliya
  • 624
  • 6
  • 12
0

If all you need a format, then here it is:

Map<String, String> headers = {
   'cookie': 'cookie1=$data1;cookie2=data2;cookie3=data3'
};

can access by calling cookie1,2,3

Younes Charfaoui
  • 1,059
  • 3
  • 10
  • 20
salam
  • 9
  • 1
0

Depending if you use flutter web or mobile there different ways to get cookies

for flutter web you just have to:

  • set credentials to true on front
  • in my case add "'Access-Control-Allow-Credentials', true" to your header on server, I use Dart on back-end (Alfred framework)
  • check whether origin from your server match with your host:port on front

Your browser will do the job and resend automatically your cookies.

you can define a specific port when launching the app with this command - "flutter run -d chrome --web-port 5555"

But for mobile you have to make some tricks

I use Dio package to easily define an onResponse/onRequest function and a conditional import to avoid compilation fail. (withCredentials option is available only on web unfortunately)

If you want to use default http class

you just have to make your own onResponse/onRequest function

NetworkConfig.dart

import 'package:dio/dio.dart';

import '../../../constants/url_paths.dart';
import 'get_net_config.dart'
    if (dart.library.io) 'mobile_net_config.dart'
    if (dart.library.html) 'web_net_config.dart';

class NetworkConfig {
  final _client = getClient()
    ..options = BaseOptions(
      baseUrl: url,
      connectTimeout: const Duration(seconds: 5),
      receiveTimeout: const Duration(seconds: 6),
    );

  Dio get client => _client;

  final Map<String, String> headers = <String, String>{
    'Content-Type': 'application/json'
  };
}

I use another class to do my get, post... which extends NetworkConfig

get_network_config.dart

import 'package:dio/dio.dart';

    Dio getClient() => throw UnsupportedError('[Platform ERROR] Network client');

web_network_config.dart

import 'package:dio/browser.dart';
import 'package:dio/dio.dart';

Dio getClient() =>
    Dio()..httpClientAdapter = BrowserHttpClientAdapter(withCredentials: true);

mobile_network_config.dart

import 'dart:io';
import 'package:<projet_name>/data/data.dart';
import 'package:dio/dio.dart';

// CLIENT
Dio getClient() => Dio()
  ..interceptors.add(InterceptorsWrapper(
    onRequest: (options, handler) async {
      options.headers['cookie'] = await localData.read('cookie');

      return handler.next(options);
    },
    onResponse: (response, handler) {
      response.headers.forEach((name, values) async {
        if (name == HttpHeaders.setCookieHeader) {
          final cookieMap = <String, String>{};

          for (var c in values) {
            var key = '';
            var value = '';

            key = c.substring(0, c.indexOf('='));
            value = c.substring(key.length + 1, c.indexOf(';'));

            cookieMap[key] = value;
          }

          var cookiesFormatted = '';

          cookieMap
              .forEach((key, value) => cookiesFormatted += '$key=$value; ');

          await localData.write('cookie', cookiesFormatted);

          return;
        }
      });

      return handler.next(response);
    },
  ));

localData is my wrapper for flutter_secure_storage (to persiste cookies locally)


if you use the default Client() class you can also set credentials like this

import 'package:http/http.dart';

Client getClient() => BrowserClient()..withCredentials = true;
bk3
  • 41
  • 3