You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter Web登录仅发OPTIONS无POST,Go服务返回200异常排查

问题概述

登录模块代码一年未改动,某次更新后Flutter Web登录页面仅发送OPTIONS请求(Go API服务返回200),无POST请求到达服务端,开发与生产环境均出现此问题,但Linux客户端登录正常。已按相关教程配置CORS但无效,GET请求可正常执行。

报错信息

ClientException: XMLHttpRequest error., uri=https://api.mydomain.com/login

相关代码

Go服务端CORS配置代码

r := chi.NewRouter()

r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.URLFormat)
r.Use(render.SetContentType(render.ContentTypeJSON))
r.Use(cors.Handler(cors.Options){
    AllowedOrigins:   []string{"https://*, http://*"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Accept", "Origin", "Authorization", "Content-Type", "X-CSRF-Token", "X-Requested-With"},
    ExposedHeaders:   []string{"Link"},
    AllowCredentials: false,
    MaxAge:           300, // Maximum value not ignored by any of major browsers
}))

Flutter登录请求代码

Future<bool> login(String email, String password, String fcmToken) async {
 try {
  var response = await http.post(
    Uri.parse("$_baseUrl/login"),
    headers: <String, String>{
      'Content-Type': 'application/json; charset=UTF-8',
    },
    body: jsonEncode(
      <String, String>{
        'business_email': email,
        'password': password,
        'token': fcmToken,
      },
    ),
  );

  if (response.statusCode == 200) {
    String jwt = response.body;

    await storage.write(key: 'jwt', value: jwt);

    return true;
  }
} catch (e) {
  logger.d(e);
  return false;
}

return false;
}

Flutter登录页面代码

class Login extends StatefulWidget {
  const Login({
    Key? key,
  }) : super(key: key);

  @override
  State<Login> createState() => _LoginState();
}

class _LoginState extends State<Login> {
  @override
  void initState() {
    WidgetsBinding.instance.addPostFrameCallback(
      (_) => showSnackBar(context),
    );

    super.initState();
  }

  final GlobalKey<FormState> _formKey = GlobalKey();

  final FocusNode _focusNodePassword = FocusNode();
  final TextEditingController _controllerUserEmail = TextEditingController();
  final TextEditingController _controllerPassword = TextEditingController();

  bool _obscurePassword = true;

  bool isLoading = false;

  String? fcmToken = '';

  @override
  Widget build(BuildContext context) {
    var model = LoginModel(
      authenticationService: Provider.of(context, listen: false),
    );

    return Scaffold(
      body: Form(
        key: _formKey,
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(30.0),
          child: Column(
            children: [
              const SizedBox(
                height: 20,
              ),
              SizedBox(
                  width: double.infinity,
                  child: Text(
                    'exactCASE',
                    textAlign: TextAlign.center,
                    style: Theme.of(context).textTheme.headlineLarge,
                  )),
              const SizedBox(height: 30),
              isLoading
                  ? const Center(child: CircularProgressIndicator())
                  : Text(
                      'Login to your account',
                      style: Theme.of(context).textTheme.labelMedium,
                    ),
              const SizedBox(height: 20),
              TextFormField(
                controller: _controllerUserEmail,
                keyboardType: TextInputType.name,
                decoration: InputDecoration(
                  labelText: 'Email',
                  prefixIcon: const Icon(Icons.person_outline),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),
                onEditingComplete: () => _focusNodePassword.requestFocus(),
                validator: (String? value) {
                  if (value == null || value.isEmpty) {
                    return "Please enter your email";
                  }
                  return null;
                },
              ),
              const SizedBox(height: 10),
              TextFormField(
                controller: _controllerPassword,
                focusNode: _focusNodePassword,
                obscureText: _obscurePassword,
                keyboardType: TextInputType.visiblePassword,
                decoration: InputDecoration(
                  labelText: "Password",
                  prefixIcon: const Icon(Icons.password_outlined),
                  suffixIcon: IconButton(
                      onPressed: () {
                        setState(() {
                          _obscurePassword = !_obscurePassword;
                        });
                      },
                      icon: _obscurePassword
                          ? const Icon(Icons.visibility_outlined)
                          : const Icon(Icons.visibility_off_outlined)),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),
                validator: (String? value) {
                  if (value == null || value.isEmpty) {
                    return "Please enter password.";
                  }

                  return null;
                },
              ),
              const SizedBox(height: 30),
              Column(
                children: [
                  FilledButton(
                    style: FilledButton.styleFrom(
                      minimumSize: const Size.fromHeight(50),
                      shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(20),
                      ),
                    ),
                    onPressed: () {
                      if (_formKey.currentState?.validate() ?? false) {
                        _login(context, model, _controllerUserEmail.text,
                            _controllerPassword.text);
                      }
                    },
                    child: const Text("Login"),
                  ),
                ],
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _focusNodePassword.dispose();
    _controllerUserEmail.dispose();
    _controllerPassword.dispose();
    super.dispose();
  }

  Future<void> _login(BuildContext context, LoginModel model, String email,
      String password) async {
    final navigator = Navigator.of(context);

    if (defaultTargetPlatform != TargetPlatform.linux &&
        defaultTargetPlatform != TargetPlatform.windows) {
      fcmToken = await FirebaseMessaging.instance.getToken();
    }

    setState(
      () {
        isLoading = true;
      },
    );

    fcmToken ??= '';

    var loginSuccess = await model.login(email, password, fcmToken!);

    if (loginSuccess) {
      navigator.pushNamed(RoutePaths.homeTabs);
    } else {
      setState(
        () {
          isLoading = false;
          final snackBar = SnackBar(
              content: const Text('Login Failed!'),
              action: SnackBarAction(
                label: 'OK',
                onPressed: () {},
              ));

          ScaffoldMessenger.of(context).showSnackBar(snackBar);
        },
      );
    }
  }
}
解决方案

1. 修复CORS通配符格式

Go chi的cors中间件中,AllowedOrigins的通配符写法错误,需将合并的字符串拆分为独立项:

AllowedOrigins:   []string{"https://*", "http://*"},

原写法会被识别为单个无效域名规则,无法匹配所有HTTPS/HTTP来源,导致浏览器拦截后续POST请求。

2. 调整中间件注册顺序

确保CORS中间件是第一个全局注册的中间件,避免其他中间件在CORS处理前返回响应:

r := chi.NewRouter()
// 优先注册CORS中间件
r.Use(cors.Handler(cors.Options{
    AllowedOrigins:   []string{"https://*", "http://*"},
    AllowedMethods:   []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
    AllowedHeaders:   []string{"Accept", "Origin", "Authorization", "Content-Type", "X-CSRF-Token", "X-Requested-With"},
    ExposedHeaders:   []string{"Link"},
    AllowCredentials: false,
    MaxAge:           300,
}))
// 再注册其他中间件
r.Use(middleware.RequestID)
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.URLFormat)
r.Use(render.SetContentType(render.ContentTypeJSON))

3. 验证OPTIONS响应头

通过浏览器开发者工具的Network面板,检查OPTIONS请求的响应头是否包含:

  • Access-Control-Allow-Origin:匹配请求的Origin
  • Access-Control-Allow-Methods:包含POST
  • Access-Control-Allow-Headers:包含Content-Type

4. 额外排查点

  • 测试时使用具体域名而非通配符,排除通配符解析问题
  • 关闭浏览器的广告拦截、CORS相关插件,排除干扰
  • 升级Flutter的http包到最新版本,排除版本兼容性问题

内容的提问来源于stack exchange,提问作者markhorrocks

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.12 06:25:58