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

Flutter Web调用Node.js API遇ERR_SSL_PROTOCOL_ERROR问题求助

Flutter Web部署后调用同服务器Node.js API出现SSL协议错误

我有一个部署在Web服务器上的Flutter Web应用,需要从同一服务器的Node.js API获取数据。输入物品编号后,应用本应调用API返回对应物品描述,但Chrome开发者日志报错:

Failed to fetch item description: ClientException: XMLHttpRequest error., uri=ERR_SSL_PROTOCOL_ERROR

已尝试的解决方案:

  • 在index.html的head区域添加相关标签(未添加时会出现混合内容错误,提示API需通过HTTPS访问);
  • 在API代码中配置了CORS。

该配置在本地运行正常,但部署到服务器后失效。以下是我的Web API代码和Flutter页面代码:

Node.js API代码

const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const sql = require('mssql');

const app = express();
const port = 3000;

// Database configuration
const config = {
    user: 'db username',
    password: 'db password',
    server: 'db server', // Your database server
    database: 'db name ',
    options: {
        enableArithAbort: true,
        encrypt: true, // Use encryption for this connection
        trustServerCertificate: true // Trust the self-signed certificate
    },
};

// Middleware
app.use(cors()); // Setup CORS for all domains
app.use(bodyParser.json()); // Parse JSON bodies

// Endpoint to get an item description by CPSNumber
app.get('/surplusItems', async (req, res) => {
    const { CPSNumber } = req.query;
    if (!CPSNumber) {
        return res.status(400).json({ error: "CPSNumber parameter is missing" });
    }

    try {
        await sql.connect(config);
        const result = await sql.query`SELECT Description FROM SurplusItemsNew WHERE CPSNumber = ${CPSNumber}`;
        sql.close();

        if (result.recordset.length === 0) {
            return res.status(404).json({ error: "No item found with the given CPSNumber" });
        }

        res.json(result.recordset[0]);
    } catch (error) {
        console.error("Error occurred:", error);
        sql.close();
        return res.status(500).json({ error: "Internal server error" });
    }
});

// Endpoint to submit multiple surplus items
app.post('/submitSurplusItems', async (req, res) => {
    const items = req.body.items;
    if (!items) {
        return res.status(400).json({ error: "No items provided" });
    }

    try {
        await sql.connect(config);
        const transaction = new sql.Transaction();
        await transaction.begin();
        const request = new sql.Request(transaction);

        for (const item of items) {
            await request.query`INSERT INTO SurplusItemsNew (BuildingName, CPSNumber, Description, ReasonDescription, SurplusStatus) VALUES (${item.school}, ${item.cpsNumber}, ${item.itemDescription}, ${item.selectedReason}, 1)`;
        }

        await transaction.commit();
        res.json({ success: true, message: "Items successfully inserted." });
    } catch (error) {
        console.error("Error occurred:", error);
        res.status(500).json({ success: false, error: "Internal server error" });
    }
});

// Start the server
app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
});

Flutter页面代码

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:surplus/form_data.dart';
import 'package:surplus/summary.dart';
import 'package:http/http.dart' as http;

class FormPage extends StatefulWidget {
  @override
  _FormPageState createState() => _FormPageState();
}

class _FormPageState extends State<FormPage> {
  List<FormData> forms = [];
  String? _selectedSchool;
final List<String> _schools = [
  'Alpha Hart Lewis Elementary',
  'Battle High School',
  'Benton Elementary',
  'Beulah Ralph Elementary',
  'Blue Ridge Elementary',
  'Business Office',
  'CORE-Boys and Girls Town',
  'CORE-Quest',
  'Cedar Ridge Elementary',
  'Center for Early Learning North',
  'Columbia Area Career Center',
  'Derby Ridge Elementary',
  'Douglass High Adult Basic Ed',
  'Douglass High School',
  'Early Childhood Discovery Cntr',
  'Early Childhood Special Ed',
  'Eliot Battle Elementary',
  'Facilities & Construction Srv',
  'Fairview Elementary',
  'Field Building',
  'Gentry Middle School',
  'Gifted Center',
  'Grant Elementary',
  'Hickman High School',
  'Jefferson Middle School',
  'John Warner Middle School',
  'Juvenile Justice Center',
  'Lange Middle School',
  'Locust Street Expressive Arts Elementary',
  'Mail Check',
  'Midway Heights Elementary',
  'Mill Creek Elementary',
  'Moving On Together',
  'New Haven Elementary',
  'Nutrition Services',
  'Oakland Middle School',
  'Park Avenue Head Start',
  'Parkade Elementary',
  'Paxton-Keeley Elementary',
  'Rainforest School',
  'Ridgeway Elementary',
  'Rock Bridge Elementary',
  'Rock Bridge High School',
  'Rosetta Avenue Learning Center',
  'Russell Blvd Elementary',
  'Shepard Blvd Elementary',
  'Smithton Middle School',
  'Two Mile Prairie Elementary',
  'West Blvd Elementary',
  'West Middle School',
];


  @override
  void initState() {
    super.initState();
    forms.add(FormData());
  }

  void addNewForm() {
    setState(() {
      forms.add(FormData());
    });
  }

void reviewData(BuildContext context) async {
  Navigator.push(
    context,
    MaterialPageRoute(
      builder: (context) => SummaryPage(
        formData: forms.where((element) => element.cpsNumber.isNotEmpty).toList(),
        school: _selectedSchool ?? "",
      ),
    ),
  );
}


  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('Surplus Asset Disposal Form'),
          centerTitle: true,
      foregroundColor: Colors.white, backgroundColor: Color(0xFF005570), // Set text color to white

        ),
        body: Column(
          children: [
            Padding(
              padding: EdgeInsets.all(16.0),
              child: DropdownButtonFormField<String>(
                value: _selectedSchool,
                decoration: InputDecoration(
                  labelText: 'Select School',
                  border: OutlineInputBorder(),
                ),
                onChanged: (String? newValue) {
                  setState(() {
                    _selectedSchool = newValue;
                  });
                },
                items: _schools.map<DropdownMenuItem<String>>((String value) {
                  return DropdownMenuItem<String>(
                    value: value,
                    child: Text(value),
                  );
                }).toList(),
              ),
            ),
            Expanded(
              child: ListView.builder(
                itemCount: forms.length,
                itemBuilder: (context, index) => MyCustomForm(
                  formData: forms[index],
                  onCpsNumberTyped: addNewForm,
                  onDataUpdated: (data) {
                    setState(() => forms[index] = data);
                  },
                ),
              ),
            ),
            Padding(
              padding: const EdgeInsets.symmetric(vertical: 16.0),
              child: ElevatedButton(
                onPressed: () => reviewData(context),
                child: Text('REVIEW'),
              style: ElevatedButton.styleFrom(
            foregroundColor: Colors.white, backgroundColor: Color(0xFF005570), // Set text color to white

            ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}



class MyCustomForm extends StatefulWidget {
  final FormData formData;
  final VoidCallback onCpsNumberTyped;
  final Function(FormData) onDataUpdated;

  MyCustomForm({required this.formData, required this.onCpsNumberTyped, required this.onDataUpdated});

  @override
  _MyCustomFormState createState() => _MyCustomFormState();
}

class _MyCustomFormState extends State<MyCustomForm> {
  final TextEditingController _cpsNumberController = TextEditingController();
  final TextEditingController _itemDescriptionController = TextEditingController();
  String? selectedReason;
  bool _isFirstInput = true;

final List<String> _reasonsForSurplus = [
  'No Longer Used',
  'Burned Out',
  'Broken',
  'Extinction',
  'Obsolete',
];


  @override
  void initState() {
    super.initState();
    _cpsNumberController.text = widget.formData.cpsNumber;
    _itemDescriptionController.text = widget.formData.itemDescription;
    selectedReason = widget.formData.selectedReason;
    _cpsNumberController.addListener(() {
      final cpsNumber = _cpsNumberController.text;
      if (cpsNumber.isNotEmpty && _isFirstInput) {
        _fetchItemDescription(cpsNumber);
        widget.onCpsNumberTyped();
        _isFirstInput = false; // Prevents continuous addition of forms
      }
      widget.onDataUpdated(widget.formData.copyWith(cpsNumber: cpsNumber));
    });
    _itemDescriptionController.addListener(() {
      widget.onDataUpdated(widget.formData.copyWith(itemDescription: _itemDescriptionController.text));
    });
  }

void _fetchItemDescription(String cpsNumber) async {
  // Update the URL to match the new API endpoint.
  // Assuming your server is running locally and accessible from your Flutter app,
  // you might need to replace 'localhost' with your local IP address when running on a real device.
  //final url = Uri.parse('http://10.250.250.101:3000/surplusItems?CPSNumber=$cpsNumber');
  final url = Uri.parse('http://web.cpsk12.org:3000/surplusItems?CPSNumber=$cpsNumber');

  try {
    final response = await http.get(url);
    if (response.statusCode == 200) {
      final data = json.decode(response.body);
      if (data != null && data['Description'] != null) {
        setState(() {
          _itemDescriptionController.text = data['Description'];
          widget.onDataUpdated(widget.formData.copyWith(itemDescription: data['Description']));
        });
      } else {
        setState(() {
          _itemDescriptionController.text = 'Description not found';
          widget.onDataUpdated(widget.formData.copyWith(itemDescription: 'Description not found'));
        });
      }
    } else {
      print('Server error: ${response.statusCode}');
      // It's a good practice to handle HTTP errors gracefully.
      setState(() {
        _itemDescriptionController.text = 'Error fetching description';
        widget.onDataUpdated(widget.formData.copyWith(itemDescription: 'Error fetching description'));
      });
    }
  } catch (e) {
    print('Failed to fetch item description: $e');
    setState(() {
      _itemDescriptionController.text = 'Failed to connect';
      widget.onDataUpdated(widget.formData.copyWith(itemDescription: 'Failed to connect'));
    });
  }
}



  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16.0),
      child: Card(
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: <Widget>[
              TextFormField(
                controller: _cpsNumberController,
                decoration: InputDecoration(
                  labelText: 'CPS Number',
                  border: OutlineInputBorder(),
                ),
                onFieldSubmitted: (value) {
                  // Trigger fetchItemDescription when Enter key is pressed
                  _fetchItemDescription(value);
                },
              ),
              SizedBox(height: 24.0),
              TextFormField(
                controller: _itemDescriptionController,
                decoration: InputDecoration(
                  labelText: 'Item Description',
                  border: OutlineInputBorder(),
                ),
                maxLines: null,
              ),
              SizedBox(height: 24.0),
              DropdownButtonFormField<String>(
                value: selectedReason,
                decoration: InputDecoration(
                  labelText: 'Reason for Surplus',
                  border: OutlineInputBorder(),
                ),
                onChanged: (String? newValue) {
                  setState(() {
                    selectedReason = newValue;
                    widget.onDataUpdated(widget.formData.copyWith(selectedReason: newValue));
                  });
                },
                items: _reasonsForSurplus.map<DropdownMenuItem<String>>((String value) {
                  return DropdownMenuItem<String>(
                    value: value,
                    child: Text(value),
                  );
                }).toList(),
              ),
            ],
          ),
        ),
      ),
    );
  }

  @override
  void dispose() {
    _cpsNumberController.dispose();
    _itemDescriptionController.dispose();
    super.dispose();
  }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 19:19:50