PAYATHON 2026

Django 或 PHP cURL POST XML 到支付网关时出错

支付老李

明确结论

从文档里的 xml=xml file content 来看,该接口更可能要求提交一个名为 xml 的参数,参数值是完整的 XML,而不是把 XML 直接写入 POST 请求体:

xml=<完整的 XML 内容>

建议先尝试以下格式:

POST /wscobroSdos/CobroAirlines
Content-Type: application/x-www-form-urlencoded

xml=%3C...URL 编码后的 XML...%3E

现有代码发送的是裸 XML,URL 中的 ?xml= 却没有值。网关读取 xml 参数时,很可能只能得到空字符串,因此返回:

El xml enviado no corresponde a ningun modelo

这句话的意思是“发送的 XML 不符合任何模型”。原因可能是网关根本没有收到 xml 参数,也可能是 XML 已经收到,但根元素、节点结构或字段格式不符合协议。

为什么当前请求不符合接口格式

PHP、pycurl 和 urllib2 示例发送的请求大致都是:

POST /wscobroSdos/CobroAirlines?xml=
Content-Type: text/xml

<完整 XML>

问题有两处:

  1. 查询字符串里有 xml 参数,但值为空。
  2. XML 以原始请求体的形式发送,并不是 xml 参数的值。

如果服务端通过下面的方式取值:

request.getParameter("xml")

它不会自动读取裸 XML 请求体,只会查找查询参数或表单参数。这里更合适的 Content-Type 是:

application/x-www-form-urlencoded

除非网关文档明确说明该接口使用 SOAP,并提供了 SOAP Envelope、SOAPAction 或 WSDL,否则不应使用 application/soap+xml。目前给出的地址和 xml=... 格式更像普通的 HTTP 表单接口。

PHP cURL 的正确写法

可以用 http_build_query() 编码参数名和参数值:

<?php

$xmlData = file_get_contents('payment.xml');

if ($xmlData === false) {
    throw new RuntimeException('无法读取 payment.xml');
}

$url = 'https://dev.mitec.com.mx/wscobroSdos/CobroAirlines';

$postData = http_build_query(
    ['xml' => $xmlData],
    '',
    '&',
    PHP_QUERY_RFC3986
);

$ch = curl_init($url);

curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => $postData,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/x-www-form-urlencoded',
        'Accept: application/xml, text/xml, */*',
    ],
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_FOLLOWLOCATION => true,
    CURLOPT_CONNECTTIMEOUT => 15,
    CURLOPT_TIMEOUT => 60,
]);

$response = curl_exec($ch);

if ($response === false) {
    throw new RuntimeException(curl_error($ch), curl_errno($ch));
}

$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo "HTTP status: {$status}\n";
echo $response;

URL 不要写成:

https://dev.mitec.com.mx/wscobroSdos/CobroAirlines?xml=

否则请求中会多出一个值为空的 xml 查询参数。部分服务器遇到查询参数和 POST 参数同名时,可能优先读取查询参数中的空值,忽略 POST 中的正确内容。

除非已经确认当前 PHP 和 cURL 环境生成的请求格式符合网关要求,也不要直接使用下面的数组写法:

CURLOPT_POSTFIELDS => ['xml' => $xmlData]

这种写法在某些环境中会生成 multipart/form-data。老旧支付接口通常更容易处理 application/x-www-form-urlencoded,因此显式调用 http_build_query() 更稳妥。

Python 的推荐写法

现代 Django 或 Python 项目可以使用 requests:

from pathlib import Path

import requests

xml_data = Path("payment.xml").read_text(encoding="utf-8")

response = requests.post(
    "https://dev.mitec.com.mx/wscobroSdos/CobroAirlines",
    data={"xml": xml_data},
    headers={
        "Accept": "application/xml, text/xml, */*",
    },
    timeout=(15, 60),
)

response.raise_for_status()
print(response.text)

requests 会把字典编码为:

application/x-www-form-urlencoded

这段代码可以放在 Django 的服务层或网关客户端中。支付协议和凭证处理逻辑不宜直接堆在视图里。

如果 XML 文件不是 UTF-8,应按照网关规定的编码读取。比如 XML 声明为:

<?xml version="1.0" encoding="ISO-8859-1"?>

读取后就不能随意按 UTF-8 重新编码。XML 声明、文件的实际字节编码和 HTTP 表单编码必须保持一致,具体要求以网关文档为准。

pycurl 写法

pycurl 同样应提交经过编码的 xml 表单参数,而不是发送裸 XML:

import io
import urllib.parse

import pycurl

with open("payment.xml", "rb") as file:
    xml_bytes = file.read()

xml_text = xml_bytes.decode("utf-8")
post_body = urllib.parse.urlencode({"xml": xml_text}).encode("ascii")

response_buffer = io.BytesIO()

curl = pycurl.Curl()
curl.setopt(
    pycurl.URL,
    "https://dev.mitec.com.mx/wscobroSdos/CobroAirlines"
)
curl.setopt(pycurl.POST, 1)
curl.setopt(pycurl.POSTFIELDS, post_body)
curl.setopt(
    pycurl.HTTPHEADER,
    [
        "Content-Type: application/x-www-form-urlencoded",
        "Accept: application/xml, text/xml, */*",
    ],
)
curl.setopt(pycurl.WRITEFUNCTION, response_buffer.write)
curl.setopt(pycurl.CONNECTTIMEOUT, 15)
curl.setopt(pycurl.TIMEOUT, 60)
curl.setopt(pycurl.NOSIGNAL, 1)

curl.perform()
status = curl.getinfo(pycurl.RESPONSE_CODE)
curl.close()

print(status)
print(response_buffer.getvalue().decode("utf-8", errors="replace"))

如果项目仍在使用 Python 2,可以这样编码:

import urllib

post_body = urllib.urlencode({'xml': binaryptr})

再把 post_body 传给 pycurl.POSTFIELDS。

urllib2 写法

Python 2 的 urllib2 也应提交经过 urlencode() 编码的表单数据:

import urllib
import urllib2

with open("payment.xml", "rb") as file:
    xml_data = file.read()

post_data = urllib.urlencode({
    "xml": xml_data,
})

request = urllib2.Request(
    "https://dev.mitec.com.mx/wscobroSdos/CobroAirlines",
    data=post_data,
    headers={
        "Content-Type": "application/x-www-form-urlencoded",
        "Accept": "application/xml, text/xml, */*",
    },
)

response = urllib2.urlopen(request, timeout=60)
print(response.read())

原来的 application/soap+xml 只修改了请求头,没有把请求体封装成名为 xml 的参数,因此无法解决这个问题。

如果接口实际要求 GET

URL 示例中出现了 ?xml=,也有可能表示网关要求通过 GET 查询参数提交。仅凭现有说明无法确定,仍需查阅该接口对应版本的正式文档,或向供应商确认。

正确的 GET 格式应为:

https://dev.mitec.com.mx/wscobroSdos/CobroAirlines?xml=<URL 编码后的 XML>

PHP 示例:

<?php

$xmlData = file_get_contents('payment.xml');

$url = 'https://dev.mitec.com.mx/wscobroSdos/CobroAirlines?' .
       http_build_query(
           ['xml' => $xmlData],
           '',
           '&',
           PHP_QUERY_RFC3986
       );

$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);

$response = curl_exec($ch);

if ($response === false) {
    throw new RuntimeException(curl_error($ch), curl_errno($ch));
}

curl_close($ch);
echo $response;

Python 示例:

from pathlib import Path
from urllib.parse import urlencode

import requests

xml_data = Path("payment.xml").read_text(encoding="utf-8")

url = (
    "https://dev.mitec.com.mx/wscobroSdos/CobroAirlines?"
    + urlencode({"xml": xml_data})
)

response = requests.get(url, timeout=(15, 60))
response.raise_for_status()

print(response.text)

不过,XML 通常较长,还可能包含个人信息或支付信息,不适合写进 URL。URL 可能被代理服务器、Web 服务器、浏览器历史和监控系统记录,也可能超过服务器或代理允许的长度。除非网关明确要求 GET,否则应优先使用 POST 表单参数。

修正传输格式后仍然报错怎么办

正确提交 xml 参数只能解决传输层的问题。如果错误仍然出现,还要检查 XML 本身。

1. 确认文件是完整 XML

问题中展示的内容只有值,看不到元素标签。如果实际的 payment.xml 也是这种形式,它就不是合法的 XML。XML 文档至少要有根元素和完整标签,例如:

<?xml version="1.0" encoding="UTF-8"?>
<Payment>
    <Merchant>0000</Merchant>
    <Amount>1000</Amount>
    <Currency>MXN</Currency>
</Payment>

这只是结构示意,不能据此推断网关实际使用的元素名称。根元素、节点顺序、大小写、命名空间和必填字段都必须严格遵守网关提供的 schema、XSD 或样例文件。

2. 先做 XML 语法校验

PHP 可以在发送前执行检查:

libxml_use_internal_errors(true);

$xml = simplexml_load_string($xmlData);

if ($xml === false) {
    foreach (libxml_get_errors() as $error) {
        echo trim($error->message) . PHP_EOL;
    }

    throw new RuntimeException('payment.xml 不是合法 XML');
}

Python 可以这样检查:

from xml.etree import ElementTree

ElementTree.fromstring(xml_data)

XML 语法合法,不代表它符合支付网关的模型。如果供应商提供了 XSD,还应通过 XSD 验证节点结构和数据类型。

3. 检查 XML 声明和编码

常见问题包括:

  • XML 声明写的是 UTF-8,实际文件却采用 ISO-8859-1。
  • 内容中含有 &,但没有转义成 &amp;。
  • 字段中出现 < 或 >,却没有转义。
  • 文件开头带有 UTF-8 BOM,而旧接口无法处理。
  • 文档要求使用特定编码,程序却做了错误的字符集转换。

不要用简单的字符串替换来删除特殊字符。应通过 XML 库生成文档,由库完成正确的转义。

4. 检查协议模型

“El xml enviado no corresponde a ningun modelo” 很像应用层模型匹配失败。即使 XML 可以正常解析,以下差异仍可能导致这个错误:

  • 根元素名称不正确。
  • 使用了其他产品或接口版本的 XML 模板。
  • 节点顺序与 schema 不一致。
  • 缺少必填节点。
  • 元素名称大小写错误。
  • 命名空间缺失或错误。
  • 日期、金额、国家代码或货币代码的格式不符合规定。
  • 请求发到了错误的测试路径。
  • XML 不属于 CobroAirlines 模型。

应向支付网关索取与该 URL 完全对应的请求样例、接口版本和 XSD,不要只参考其他下载页面中的相似文档。

调试时确认实际发送内容

可以先用不含真实支付数据的测试文件,查看 cURL 实际发出的请求头和请求体:

curl_setopt($ch, CURLOPT_VERBOSE, true);

也可以通过命令行复现表单提交:

curl --verbose \
  --data-urlencode "xml@payment.xml" \
  "https://dev.mitec.com.mx/wscobroSdos/CobroAirlines"

--data-urlencode "xml@payment.xml" 会读取文件内容,将它作为 xml 参数值进行 URL 编码。这和直接发送裸文件不是一回事。

调试日志中可能包含卡号、持卡人信息、联系方式、认证字段或商户密钥。不要把完整请求写入普通应用日志,也不要将其提交到工单、代码仓库或公开论坛。

安全注意事项

生产代码中不要使用:

CURLOPT_SSL_VERIFYHOST => 0
CURLOPT_SSL_VERIFYPEER => 0

这些设置会关闭 HTTPS 证书验证,使支付请求容易受到中间人攻击。应保留证书校验并正确配置系统 CA。如果网关使用私有 CA,应安装合法的证书链,而不是关闭验证。

还要确认支付数据的采集、传输、日志记录和存储方式符合支付网关合同及适用的 PCI DSS 要求。日志中不要保存完整卡号、安全码、认证令牌或未经脱敏的 XML。

建议先去掉 URL 末尾的 ?xml=,改用 POST,并把 XML 作为 application/x-www-form-urlencoded 请求中的 xml 参数提交。如果错误依旧存在,就检查 XML 模型,不必继续更换 PHP、pycurl 或 urllib2 客户端。

备注:内容仅供参考。