Flutter填坑筆記: dio報錯 FormatException

TonyBuilder發表於2019-09-03

  以前說到, Dart 是個年輕的語言,SDK 還不夠成熟,使用中有很多坑。之前解決 了一個使用代理導致空指標的問題,這次又碰上了一個使用 Cookie 產生 FormatException 的問題。

問題描述

   dio是Flutter中文網開源的一個強大的Dart Http請求庫,我使用這個庫寫了 一個訪問網站 Login 介面的 Demo。

  Dio client = new Dio(options);
  client.interceptors.add(CookieManager(CookieJar()));
  
  FormData formData = new FormData.from(
      {"id": 123456, "passwd": 456789, "CookieDate": "2"});
  
  var response = await client.post<String>(
      "/path/to/login.json",
      data: formData,
  );
複製程式碼

程式碼很簡單:

  • 使用 dio 發起一個 Post 請求,包含使用者名稱和密碼,呼叫 web login 介面;
  • 伺服器response 包含 set-cookie 設定登入資訊; 後續訪問需要攜帶 cookie.
  • 客戶端使用 CookieManager(CookieJar()) 儲存 cookie資訊。

然而很不幸,簡單的程式碼遇上了如下錯誤:

DioError [DioErrorType.DEFAULT]: FormatException: Invalid character in cookie name, code unit: '91' (at character 5) main[UTMPUSERID]

抓出伺服器響應分析:

set-cookie: main[UTMPUSERID]=guest; path=/; domain=.****.net

set-cookie: main[UTMPKEY]=48990095; path=/; domain=.****.net

set-cookie: main[UTMPNUM]=79117; path=/; domain=.****.net

set-cookie 欄位包含了非法字元"["和"]",導致請求失敗。

  問題已經定位,請伺服器兄弟吃頓燒烤,改一下 set-cookie 字串定義,問題解決。 ^_^

  但是作為一個有追求的程式設計師,不能不跟蹤一下根本原因。

問題定位

協議中關於 set-cookie 的規定

   HTTP 協議中,關於 set-cookie 可以使用的字元有明確規定:

RFC6265 中規定 cookie-name 是一個 token。

RFC6265

RFC2616 中定義了 token 就是 CHAR 排除掉分割符,因此"["和"]"確實是協議規定的非法字元。

RFC2616

   貌似燒烤白請了 T_T,但是協議規定和現實有很大的距離。

StackOverFlow 這篇回答解釋了 set-cookie 合法字元的變化歷史,簡單來說,RFC6265協議定義了最新的標準, 新的網路介面都應該符合這個標準。但是有大量的歷史遺留問題,很多網站採用的就是最原始的 Netscape cookie_spec。 因此,從實際角度出發,伺服器端新增介面都要符合RFC6265,而客戶端最好能向前相容歷史標準。

Dart SDK 中的處理

  回到Flutter程式碼中,Dio 通過 CookieManager 作為Intercepter 攔截所有 請求和響應,如果響應有set-cookie,就儲存在CookieJar中;發起請求時從CookieJar獲取當前 Coockie。

  dio/lib/src/interceptors/cookie_mgr.dart
  
  _saveCookies(Response response) {
    ......
        cookieJar.saveFromResponse(
          response.request.uri,
          cookies.map((str) => Cookie.fromSetCookieValue(str)).toList(),
        );
    ......
  }
複製程式碼

因此,判斷Cookie欄位是否合法,程式碼包含在 Dart SDK => Cookie.fromSetCookieValue 中:

dart-sdk/lib/_http/http_headers.dart

  void _validate() {
    const separators = const [
      "(",
      ")",
      "<",
      ">",
      "@",
      ",",
      ";",
      ":",
      "\\",
      '"',
      "/",
      "[",
      "]",
      "?",
      "=",
      "{",
      "}"
    ];
    for (int i = 0; i < name.length; i++) {
      int codeUnit = name.codeUnits[i];
      if (codeUnit <= 32 ||
          codeUnit >= 127 ||
          separators.indexOf(name[i]) >= 0) {
        throw new FormatException(
            "Invalid character in cookie name, code unit: '$codeUnit'",
            name,
            i);
      }
    }

    // Per RFC 6265, consider surrounding "" as part of the value, but otherwise
    // double quotes are not allowed.
    int start = 0;
    int end = value.length;
    if (2 <= value.length &&
        value.codeUnits[start] == 0x22 &&
        value.codeUnits[end - 1] == 0x22) {
      start++;
      end--;
    }

    for (int i = start; i < end; i++) {
      int codeUnit = value.codeUnits[i];
      if (!(codeUnit == 0x21 ||
          (codeUnit >= 0x23 && codeUnit <= 0x2B) ||
          (codeUnit >= 0x2D && codeUnit <= 0x3A) ||
          (codeUnit >= 0x3C && codeUnit <= 0x5B) ||
          (codeUnit >= 0x5D && codeUnit <= 0x7E))) {
        throw new FormatException(
            "Invalid character in cookie value, code unit: '$codeUnit'",
            value,
            i);
      }
    }
  }
複製程式碼
  • 從上面程式碼我們可以看出,Dart SDK 嚴格實現了 RFC6265 標準,
  • "(",")","<",">","@",",",";",":","\",'"',"/","[","]","?","=","{","}" 都是非法字元。
  • 注意,Dart 2.1 之前的版本 cookie name 前後如果有雙引號,也會被判斷為非法字元, 後來提了 patch 才修正。

終端規避方案

  由於伺服器程式碼祖傳,無法修改。我們在客戶端作相容,相容的方法就是,不使用 Dart SDK 提供的 Cookie,使用我們自定義的 Cookie。這樣我們可以自定義客戶端的合法字元。

Dio 建立自定義的CookieManager, PrivateCookieManager程式碼在 這個路徑

client.interceptors.add(PrivateCookieManager(CookieJar()));
......
class PrivateCookieManager extends CookieManager {
  ......
        cookieJar.saveFromResponse(
          response.request.uri,
          cookies.map((str) => _Cookie.fromSetCookieValue(str)).toList(),
        );
  ......
}

class _Cookie implements Cookie {
  void _validate() {
    const separators = const [
      "(",
      ")",
      "<",
      ">",
      "@",
      ",",
      ";",
      ":",
      "\\",
      '"',
      "/",
//******* [] is valid in this application ***********      
//      "[",
//      "]",
      "?",
      "=",
      "{",
      "}"
    ];
  }
}
複製程式碼

問題總結

  跟蹤下來,Dart SDK的處理沒有問題,符合協議要求。只是處理的灰度不夠, 畢竟現在有大量的伺服器應用還是採用的原有定義。Cookie中的_validate是一個私有方法, 如果能暴露出來可以繼承修改,冗餘程式碼量會少很多。

相關文章