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>
问题有两处:
- 查询字符串里有
xml参数,但值为空。 - 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。 - 内容中含有
&,但没有转义成&。 - 字段中出现
<或>,却没有转义。 - 文件开头带有 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 客户端。
备注:内容仅供参考。
