在 HttpClient 中使用 emojis 时显示无效字符使请求变得混乱

Showing invalid characters while using emojis in HttpClient put request in flutter

第一:我的服务器允许 PUT 操作

我在用flutter写一个记忆app(移植swiftUI版本)遇到如下错误

[VERBOSE-2:ui_dart_state.cc(198)] Unhandled Exception: Invalid argument (string): Contains invalid characters.:
"[{\"name\":\"user\",\"pass\":\"123\",\"udf\":[{\"colorRed\":255,\"colorGreen\":82,\"colorBlue\":82,\"colorAlpha\":255,\"name\":\"V
ehicles\",\"emojis\":[\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"✈️\",\"\",\"⛵️\",\"\",\"\",\"\",\"\",\"
\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"],\"chals\":[{\"id\":\"Vehicles-1\",\"disp\":1},{\"id\":\"Vehicles-2\",\"disp\":
2},{\"id\":\"Vehicles-3\",\"disp\":3},{\"id\":\"Vehicles-4\",\"disp\":4},{\"id\":\"Vehicles-5\",\"disp\":5},{\"id\":\"Vehicles-6\",
\"disp\":6},{\"id\":\"Vehicles-7\",\"disp\":7},{\"id\":\"Vehicles-8\",\"disp\":8},{\"id\":\"Vehicles-9\",\"disp\":9},{\"id\":\"Vehi
cles-10\",\"disp\":10},{\"id\":\"Vehicles-11\",\"disp\":11},{\"id\":\"Vehicles-12\",\"disp\":12},{\"id\":\"Vehicles-13\",\"disp\":1
3},{\"id\":\"Vehicles-14\",\"disp\":14},{\"id\":\"Vehicles-15\",\"d<…>

这是flutter doctor --verbose

的结果
flutter doctor --verbose                                                  ─╯
[✓] Flutter (Channel beta, 2.13.0-0.4.pre, on macOS 12.3 21E230 darwin-arm,
    locale zh-Hans-CN)
    • Flutter version 2.13.0-0.4.pre at /Users/jett/flutter
    • Upstream repository https://gitee.com/mirrors/Flutter
    • FLUTTER_GIT_URL = https://gitee.com/mirrors/Flutter
    • Framework revision 25caf1461b (4 weeks ago), 2022-05-05 14:23:09 -0700
    • Engine revision c5caf749fe
    • Dart version 2.17.0 (build 2.17.0-266.8.beta)
    • DevTools version 2.12.2
    • Pub download mirror https://pub.flutter-io.cn
    • Flutter download mirror https://storage.flutter-io.cn

[✓] Android toolchain - develop for Android devices (Android SDK version 31.0.0)
    • Android SDK at /Users/jett/Library/Android/sdk
    • Platform android-31, build-tools 31.0.0
    • Java binary at:
      /Library/Java/JavaVirtualMachines/jdk-11.0.15.1.jdk/Contents/Home/bin/java
    • Java version Java(TM) SE Runtime Environment 18.9 (build
      11.0.15.1+2-LTS-10)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 13.4.1)
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • CocoaPods version 1.11.3

[✓] Chrome - develop for the web
    • CHROME_EXECUTABLE = /Applications/Google Chrome
      Canary.app/Contents/MacOS/Google Chrome Canary

[!] Android Studio
    • Android Studio at /Applications/Android Studio Preview.app/Contents
    • Flutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dart
    ✗ Unable to find bundled Java version.
    • Try updating or re-installing Android Studio.

[✓] VS Code (version 1.68.0-insider)
    • VS Code at /Applications/Visual Studio Code - Insiders.app/Contents
    • Flutter extension version 3.42.0

[✓] Connected device (3 available)
    • iPhone 13 (mobile) • 2A8678B1-09B1-4B43-926F-BA04E5D00790 • ios
      • com.apple.CoreSimulator.SimRuntime.iOS-15-5 (simulator)
    • macOS (desktop)    • macos                                • darwin-arm64
      • macOS 12.3 21E230 darwin-arm
    • Chrome (web)       • chrome                               • web-javascript
      • Google Chrome 104.0.5102.0 canary

[✓] HTTP Host Availability
    • All required HTTP hosts are available

! Doctor found issues in 1 category.

忘记 FLUTTER_GIT_URLPub download mirrorFlutter download mirror


相关代码(我在等待调用这个函数):

static Future<void> saveUsers() async {
    final HttpClient client = HttpClient();
    final HttpClientRequest request = await client.putUrl(Preferences.uri);
    request.write(const JsonEncoder().convert(Preferences.users.map((e) => e.toJson()).toList()));
    await request.close();
}
// inside the Preferences class

变量和类:

class User {
  String name = "";
  String pass = "";
  List<Theme> udf = [];

  User();
  User.values(this.name, this.pass, this.udf);

  int match(List<User> users) {
    for(int i = 0; i < users.length; i++) {
      if(users[i].name == name && users[i].pass == pass) {
        return i;
      }
    }
    return -1;
  }

  Map<String, dynamic> toJson() => {
    "name": name,
    "pass": pass,
    "udf": udf.map((e) => e.toJson()).toList(),
  };

  String stringfy() => const JsonEncoder().convert(toJson());

  factory User.fromJson(Map<String, dynamic> mp) => User.values(
    mp["name"] as String,
    mp["pass"] as String,
    (mp["udf"] as List<dynamic>).map((e) => Theme.fromJson(e as Map<String, dynamic>)).toList(),
  );

  factory User.fromString(String s) => User.fromJson(const JsonDecoder().convert(s));
}

class Theme {
  static List<Color> availableColors = [Colors.redAccent, Colors.blueAccent, Colors.cyan, Colors.orangeAccent, Colors.deepPurple, Colors.green, Colors.indigo, Colors.lime, Colors.pinkAccent.shade100];

  Color color = Colors.redAccent;
  String name = "";
  List<String> emojis = [];
  final int id;
  List<Challange> chals = [];
  int gotToChal = 0;
  int score = 0;

  static int idCount = 0;

  Theme() : id = Theme.idCount {
    idCount += 1;
  }
  Theme.values(this.color, this.name, this.emojis, this.chals, this.gotToChal, this.score) : id = Theme.idCount {
    idCount += 1;
  }

  int match(List<Theme> themes) {
    for(int i = 0; i < themes.length; i++) {
      if(themes[i].id == id) {
        return i;
      }
    }
    return -1;
  }

  Map<String, dynamic> toJson() => {
    'colorRed': color.red,
    'colorGreen': color.green,
    'colorBlue': color.blue,
    'colorAlpha': color.alpha,
    'name': name,
    'emojis': emojis,
    'chals': chals.map((e) => e.toJson()).toList(),
    'gotToChal': gotToChal,
    'score': score,
  };

  String stringfy() => const JsonEncoder().convert(toJson());

  factory Theme.fromJson(Map<String, dynamic> mp) => Theme.values(
    Color.fromARGB(
      mp['colorAlpha'] as int,
      mp['colorRed'] as int,
      mp['colorGreen'] as int,
      mp['colorBlue'] as int
    ),
    mp['name'] as String,
    (mp['emojis'] as List<dynamic>).map((e) => e as String).toList(),
    (mp['chals'] as List<dynamic>).map((e) => Challange.fromJson(e as Map<String, dynamic>)).toList(),
    mp['gotToChal'] as int,
    mp['score'] as int,
  );

  factory Theme.fromString(String s) => Theme.fromJson(const JsonDecoder().convert(s));

  static final List<Theme> defaultThemes = [
    Theme.values(
      Colors.redAccent,
      "Vehicles",
      ["", "", "", "", "", "", "", "", "✈️", "", "⛵️", "", "", "", "", "", "", "", "", "", "", "", ""],
      List.generate(21, (index) => index+1).map((e) => Challange.preset("Vehicles-$e", e)).toList(),
      0,
      0,
    ),
    Theme.values(
      Colors.orangeAccent,
      "Fast Food",
      ["", "", "", "", "", "", "", "", "", "", "", ""],
      List.generate(10, (index) => index+1).map((e) => Challange.preset("FastFood-$e", e)).toList(),
      0,
      0,
    ),
    Theme.values(
      Colors.green,
      "Drinks",
      ["", "☕️", "", "", "", "", "", "", "", ""],
      List.generate(8, (index) => index+1).map((e) => Challange.preset("Drinks-$e", e)).toList(),
      0,
      0,
    ),
    Theme.values(
      Colors.cyan,
      "Fruits",
      ["", "", "", "", "", "", "", "", "", ""],
      List.generate(8, (index) => index+1).map((e) => Challange.preset("Fruits-$e", e)).toList(),
      0,
      0,
    ),
    Theme.values(
      Colors.blueAccent,
      "Vegetables",
      ["", "", "", "", "", "", "", ""],
      List.generate(6, (index) => index+1).map((e) => Challange.preset("Vegetables-$e", e)).toList(),
      0,
      0,
    ),
    Theme.values(
      Colors.pinkAccent.shade100,
      "Animals",
      ["", "", "", "", "", "", "", "", "", "", ""],
      List.generate(9, (index) => index+1).map((e) => Challange.preset("Animals-$e", e)).toList(),
      0,
      0,
    ),
  ];
}

class Challange {
  final String id;
  final int disp;
  Challange(this.id, this.disp);
  Challange.preset(this.id, this.disp);

  get nCards => min(min(2 + disp, 20), Game.theme.emojis.length);
  get bonusTime => Duration(milliseconds: ((6 + ((disp-1)/3))*1000).floor());

  int match(List<Challange> chals) {
    for(int i = 0; i < chals.length; i++) {
      if(chals[i].id == id) {
        return i;
      }
    }
    return -1;
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'disp': disp
  };

  String stringfy() => const JsonEncoder().convert(toJson());

  factory Challange.fromJson(Map<String, dynamic> mp) => Challange.preset(
    mp['id'] as String,
    mp['disp'] as int
  );

  factory Challange.fromString(String s) => Challange.fromJson(const JsonDecoder().convert(s));
}

// following in Preferences
static final Uri uri = Uri.parse("https://www.openwld.com/memorize.json");
static late SharedPreferences prefs;
static User user = User.values("nothing", "none", Theme.defaultThemes);
static late List<User> users;
static List<Theme> get themes => user.udf;
static set themes(List<Theme> newValue) {
  user.udf = newValue;
}
static const String _name = "memorize_user";

我要放的内容:

[{"name":"user","pass":"123","udf":[{"colorRed":255,"colorGreen":82,"colorBlue":82,"colorAlpha":255,"name":"Vehicles","emojis":["","","","","","","","","✈️","","⛵️","","","","","","","","","","","",""],"chals":[{"id":"Vehicles-1","disp":1},{"id":"Vehicles-2","disp":2},{"id":"Vehicles-3","disp":3},{"id":"Vehicles-4","disp":4},{"id":"Vehicles-5","disp":5},{"id":"Vehicles-6","disp":6},{"id":"Vehicles-7","disp":7},{"id":"Vehicles-8","disp":8},{"id":"Vehicles-9","disp":9},{"id":"Vehicles-10","disp":10},{"id":"Vehicles-11","disp":11},{"id":"Vehicles-12","disp":12},{"id":"Vehicles-13","disp":13},{"id":"Vehicles-14","disp":14},{"id":"Vehicles-15","disp":15},{"id":"Vehicles-16","disp":16},{"id":"Vehicles-17","disp":17},{"id":"Vehicles-18","disp":18},{"id":"Vehicles-19","disp":19},{"id":"Vehicles-20","disp":20},{"id":"Vehicles-21","disp":21}],"gotToChal":0,"score":0},{"colorRed":255,"colorGreen":171,"colorBlue":64,"colorAlpha":255,"name":"Fast Food","emojis":["","","","","","","","","","","",""],"chals":[{"id":"FastFood-1","disp":1},{"id":"FastFood-2","disp":2},{"id":"FastFood-3","disp":3},{"id":"FastFood-4","disp":4},{"id":"FastFood-5","disp":5},{"id":"FastFood-6","disp":6},{"id":"FastFood-7","disp":7},{"id":"FastFood-8","disp":8},{"id":"FastFood-9","disp":9},{"id":"FastFood-10","disp":10}],"gotToChal":0,"score":0},{"colorRed":76,"colorGreen":175,"colorBlue":80,"colorAlpha":255,"name":"Drinks","emojis":["","☕️","","","","","","","",""],"chals":[{"id":"Drinks-1","disp":1},{"id":"Drinks-2","disp":2},{"id":"Drinks-3","disp":3},{"id":"Drinks-4","disp":4},{"id":"Drinks-5","disp":5},{"id":"Drinks-6","disp":6},{"id":"Drinks-7","disp":7},{"id":"Drinks-8","disp":8}],"gotToChal":0,"score":0},{"colorRed":0,"colorGreen":188,"colorBlue":212,"colorAlpha":255,"name":"Fruits","emojis":["","","","","","","","","",""],"chals":[{"id":"Fruits-1","disp":1},{"id":"Fruits-2","disp":2},{"id":"Fruits-3","disp":3},{"id":"Fruits-4","disp":4},{"id":"Fruits-5","disp":5},{"id":"Fruits-6","disp":6},{"id":"Fruits-7","disp":7},{"id":"Fruits-8","disp":8}],"gotToChal":0,"score":0},{"colorRed":68,"colorGreen":138,"colorBlue":255,"colorAlpha":255,"name":"Vegetables","emojis":["","","","","","","",""],"chals":[{"id":"Vegetables-1","disp":1},{"id":"Vegetables-2","disp":2},{"id":"Vegetables-3","disp":3},{"id":"Vegetables-4","disp":4},{"id":"Vegetables-5","disp":5},{"id":"Vegetables-6","disp":6}],"gotToChal":0,"score":0},{"colorRed":255,"colorGreen":128,"colorBlue":171,"colorAlpha":255,"name":"Animals","emojis":["","","","","","","","","","",""],"chals":[{"id":"Animals-1","disp":1},{"id":"Animals-2","disp":2},{"id":"Animals-3","disp":3},{"id":"Animals-4","disp":4},{"id":"Animals-5","disp":5},{"id":"Animals-6","disp":6},{"id":"Animals-7","disp":7},{"id":"Animals-8","disp":8},{"id":"Animals-9","disp":9}],"gotToChal":0,"score":0}]}]

您使用的是默认编码,即 ISO-8859-1 而不是 UTF-8。在调用 write 之前,您需要正确设置字符集:

request.headers.contentType =
    ContentType('application', 'json', charset: 'utf-8');

Flutter 将从该字符集中检测用于序列化数据的编码。

来源:Flutter docs