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

Flutter Dart手动生成英国National Rail OpenLDBWS SOAP信封遇SOAPAction错误

解决英国国家铁路OpenLDBWS API的SOAPAction请求头错误问题

背景

首次接触WSDL与SOAP,已用Python Zeep库成功调用英国国家铁路OpenLDBWS实时时刻表API(持有有效Token),但在Dart中手动构建SOAP信封时,服务器返回HTTP 500错误,提示无法识别SOAPAction请求头的值。

Python 成功实现代码

import json
import argparse
from zeep import Client
import zeep.helpers
from zeep.transports import Transport
from requests import Session

WSDL_URL = "https://realtime.nationalrail.co.uk/OpenLDBWS/wsdl.aspx?ver=2021-11-01"

class NationalRailService:
    def __init__(self, user_token):
        self.user_token = user_token
        self.session = Session()
        self.transport = Transport(session=self.session)
        self.client = Client(WSDL_URL, transport=self.transport)
        self.service = self.client.service

        self.header = {
            "AccessToken": {
                "TokenValue": self.user_token
            }
        }

    def get_departure_board(self, from_crs, to_crs, num_rows=10):
        """
        Queries the National Rail API for departure information.

        Args:
            from_crs (str):  The departure station CRS code.
            to_crs (str): The destination station CRS code.
            num_rows (int, optional): The number of results to return. Defaults to 10.

        Returns:
            dict: A dictionary containing the query results, or an error message.
        """
        try:
            response = self.service.GetDepartureBoard(
                numRows=num_rows,
                crs=from_crs,
                filterCrs=to_crs if to_crs else None,
                filterType="to" if to_crs else None,
                _soapheaders=self.header
            )

            raw_services = (
                response.trainServices.service
                if hasattr(response.trainServices, "service")
                else []
            )

            results = [self._remove_nulls(zeep.helpers.serialize_object(s)) for s in raw_services]

            return {
                "from": from_crs,
                "to": to_crs,
                "count": len(results),
                "results": results
            }

        except Exception as e:
            return {"error": str(e)}
    
    def _remove_nulls(self, data):
        """Recursively removes null values from a dictionary."""
        if isinstance(data, dict):
            return {k: self._remove_nulls(v) for k, v in data.items() if v is not None}
        elif isinstance(data, list):
            return [self._remove_nulls(item) for item in data]
        else:
            return data

def test_response(): # test method with command line args
    parser = argparse.ArgumentParser(description="Query National Rail API")
    parser.add_argument("--token", required=True, help="Your National Rail API token")
    parser.add_argument("--from_crs", required=True, help="Departure station CRS code")
    parser.add_argument("--to_crs", required=True, help="Destination station CRS code (optional)")
    parser.add_argument("--num_rows", type=int, default=10, help="Number of results to return (default: 10)")

    args = parser.parse_args()

    event = {
        "headers": {
            "X-API-TOKEN": args.token
        },
        "queryStringParameters": {
            "from": args.from_crs,
            "to": args.to_crs,
            "numRows": args.num_rows
        }
    }

    response = lambda_handler(event)
    print(json.dumps(response, indent=2, ensure_ascii=False))

Dart 出错代码

import 'dart:developer';
import 'package:http/http.dart' as http;
import 'package:xml/xml.dart' as xml;
class NationalRailService {
  final String soapEndpoint = "https://realtime.nationalrail.co.uk/OpenLDBWS/ldb12.asmx";
  final String userToken;
  
  final Map<String, String> namespaces = {
    'soap': 'http://schemas.xmlsoap.org/soap/envelope/',
    'ldb': 'http://thalesgroup.com/RTTI/2021-11-01/ldb/',
    'typ': 'http://thalesgroup.com/RTTI/2013-11-28/Token/types'
  };

  NationalRailService(this.userToken);

  Future<Map<String, dynamic>> getDepartureBoard({
    required String fromCrs,
    String? toCrs,
    int numRows = 10,
  }) async {
    try {
      // Build SOAP envelope
      final envelope = _buildGetDepartureBoardEnvelope(
        fromCrs: fromCrs.toUpperCase(),
        toCrs: toCrs?.toUpperCase(),
        numRows: numRows,
      );

      // Send request
      final response = await http.post(
        Uri.parse(soapEndpoint),
        headers: {
          'Content-Type': 'text/xml; charset=utf-8',
          'SOAPAction': 'http://thalesgroup.com/RTTI/2021-11-01/ldb/GetDepartureBoard' // error here - HTTP Error 500: Server did not recognize the value of HTTP Header SOAPAction
        },
        body: envelope,
      );

      if (response.statusCode != 200) {
        return {
          'error': 'HTTP Error: ${response.statusCode}',
          'details': response.body
        };
      }

      // Parse XML response
      final parsedResponse = _parseGetDepartureBoardResponse(response.body);

      return {
        'from': fromCrs,
        'to': toCrs,
        'count': parsedResponse['results'].length,
        'results': parsedResponse['results'],
      };
    } catch (e) {
      return {'error': e.toString()};
    }
  }

  String _buildGetDepartureBoardEnvelope({
    required String fromCrs,
    String? toCrs,
    required int numRows,
  }) {
    return '''
    <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
                  xmlns:ldb="http://thalesgroup.com/RTTI/2021-11-01/ldb/" 
                  xmlns:typ="http://thalesgroup.com/RTTI/2013-11-28/Token/types">
      <soap:Header>
        <typ:AccessToken>
          <typ:TokenValue>$userToken</typ:TokenValue>
        </typ:AccessToken>
      </soap:Header>
      <soap:Body>
        <ldb:GetDepartureBoard>
          <ldb:numRows>$numRows</ldb:numRows>
          <ldb:crs>$fromCrs</ldb:crs>
          ${toCrs != null ? '<ldb:filterCrs>$toCrs</ldb:filterCrs><ldb:filterType>to</ldb:filterType>' : ''}
        </ldb:GetDepartureBoard>
      </soap:Body>
    </soap:Envelope>
    ''';
  }
}

问题解决方案

1. 修正SOAPAction请求头

部分SOAP服务器要求SOAPAction值用双引号包裹,将请求头修改为:

headers: {
  'Content-Type': 'text/xml; charset=utf-8',
  'SOAPAction': '"http://thalesgroup.com/RTTI/2021-11-01/ldb/GetDepartureBoard"'
}

2. 优化SOAP信封格式

移除XML字符串中的多余缩进,避免服务器解析异常。修改后的信封构建代码:

String _buildGetDepartureBoardEnvelope({
  required String fromCrs,
  String? toCrs,
  required int numRows,
}) {
  final filterContent = toCrs != null 
      ? '<ldb:filterCrs>$toCrs</ldb:filterCrs><ldb:filterType>to</ldb:filterType>' 
      : '';

  return '''
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
              xmlns:ldb="http://thalesgroup.com/RTTI/2021-11-01/ldb/" 
              xmlns:typ="http://thalesgroup.com/RTTI/2013-11-28/Token/types">
  <soap:Header>
    <typ:AccessToken>
      <typ:TokenValue>$userToken</typ:TokenValue>
    </typ:AccessToken>
  </soap:Header>
  <soap:Body>
    <ldb:GetDepartureBoard>
      <ldb:numRows>$numRows</ldb:numRows>
      <ldb:crs>$fromCrs</ldb:crs>
      $filterContent
    </ldb:GetDepartureBoard>
  </soap:Body>
</soap:Envelope>
''';
}

3. 验证命名空间与Endpoint

确认ldb命名空间末尾的斜杠完整,Endpoint地址与WSDL定义一致(当前代码中的地址正确)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 09:08:15