PHP实现带附件SOAP请求遇Bad Request,求正确配置方案
解决PHP SoapClient上传SOAP附件(MTOM/XOP)的Bad Request问题
我看了你遇到的问题,这是PHP SoapClient处理带MTOM/XOP附件的SOAP请求时的典型坑,咱们从几个核心问题入手修复:
一、核心问题分析
对比你提供的SoapUI正常请求和当前PHP代码的输出,主要问题集中在这几点:
- 请求头配置完全错误:你手动设置的
stream_context头不符合multipart/related的MTOM规范,而且头之间没有换行分隔,导致请求格式混乱。 - 未启用MTOM支持:PHP SoapClient默认不处理MTOM,需要显式开启该特性才能自动生成符合要求的多部分请求结构。
- 安全头不完整:服务端要求的
Nonce和Created字段缺失,这很可能是服务端拒绝请求的关键原因。 - 附件关联方式错误:你创建的
ImageObj没有被正确关联到SOAP请求中,XOP的Include标签无法找到对应的附件内容。
二、修改后的完整代码及说明
1. 初始化SoapClient时启用MTOM
修改initApiClient方法,移除错误的stream_context头,添加MTOM特性:
private function initApiClient($fileName) { $this->logger->dbg('Init API client', __METHOD__); // 启用MTOM、SOAP 1.2等必要特性,自动处理multipart/related请求 $options = [ 'trace' => 1, 'features' => SOAP_SINGLE_ELEMENT_ARRAYS | SOAP_MTOM | SOAP_SOAP_1_2, 'cache_wsdl' => WSDL_CACHE_NONE // 可选,避免WSDL缓存问题 ]; $this->apiClient = new SoapClient($this->wsdl, $options); $this->initHeaders($fileName); }
说明:SOAP_MTOM会让SoapClient自动处理附件的编码和多部分请求结构,无需手动拼接Content-Type等头信息。
2. 补全安全头的Nonce和Created字段
修改initHeaders方法,添加服务端要求的Nonce和Created:
private function initHeaders($fileName) { // 生成随机Nonce并Base64编码 $nonce = base64_encode(random_bytes(16)); // 生成符合ISO8601格式的时间戳 $created = gmdate('Y-m-d\TH:i:s\Z'); $userName = new SoapVar( $this->config->getApiUserName(), XSD_STRING, null, null, 'Username', $this->securityHeaderNS ); $password = new SoapVar( $this->config->getApiPassword(), XSD_STRING, null, null, 'Password', $this->securityHeaderNS, null, null, 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText' ); $nonceVar = new SoapVar( $nonce, XSD_STRING, null, null, 'Nonce', $this->securityHeaderNS, 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary' ); $createdVar = new SoapVar( $created, XSD_STRING, null, null, 'Created', 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' ); $token = new SoapVar( [$userName, $password, $nonceVar, $createdVar], SOAP_ENC_OBJECT, null, null, 'UsernameToken', $this->securityHeaderNS ); $securityHeader = new SoapHeader( $this->securityHeaderNS, 'Security', new SoapVar([$token], SOAP_ENC_OBJECT), true ); $extensionHeader = new SoapHeader( $this->extraHeaderNS, 'ExtensionId', 5000, false, $this->extraHeaderNS ); $nameHeader = new SoapHeader( $this->extraHeaderNS, 'Name', $fileName, false, $this->extraHeaderNS ); return $this->apiClient->__setSoapHeaders([ $securityHeader, $extensionHeader, $nameHeader ]); }
说明:服务端的安全验证需要Nonce(随机值防重放)和Created(请求时间),同时要给Password和Nonce指定正确的Type属性,和SoapUI请求保持一致。
3. 正确关联附件内容
修改uploadFile方法,用MTOM方式传递附件:
public function uploadFile($fileName) { $this->logger->dbg('Attempt to upload file ' . $fileName, __METHOD__); try { $this->initApiClient($fileName); // 读取文件原始内容,MTOM会自动处理编码 $fileContent = file_get_contents(__DIR__ . '/../../files/'.$fileName); // 创建XOP Include关联,同时传递原始文件内容(MTOM自动处理附件部分) $contentInclude = sprintf('<inc:Include href="cid:%s" xmlns:inc="http://www.w3.org/2004/08/xop/include"/>', $fileName); $contentVar = new SoapVar($contentInclude, XSD_ANYXML); // 构建请求参数,注意命名空间要和WSDL一致 $request = [ 'Content' => $contentVar ]; // 执行请求 $response = $this->apiClient->UploadFile($request); $this->logger->dbg('Upload success, FileId: ' . $response->FileStreamUploadResponse->FileId, __METHOD__); } catch (\Throwable $e) { $this->logger->error($e->getMessage(), __METHOD__); // 打印最后发送的请求头,方便调试 $this->logger->dbg('Last request headers: ' . $this->apiClient->__getLastRequestHeaders(), __METHOD__); } $this->logger->dbgXml(XmlHelper::formatXml($this->apiClient->__getLastRequest()), __METHOD__); $this->logger->dbgXml(XmlHelper::formatXml($this->apiClient->__getLastResponse()), __METHOD__); }
说明:启用MTOM后,SoapClient会自动将二进制文件内容作为多部分请求的一部分发送,XOP的Include标签通过cid关联对应的附件ID,无需手动处理Content-Disposition等头。
三、调试技巧
如果还是遇到问题,可以通过以下方式排查:
- 打印
__getLastRequestHeaders()查看实际发送的请求头,确认是否为multipart/related格式。 - 对比SoapUI的请求和PHP发送的请求(包括所有头和请求体),找出差异点。
- 检查WSDL定义,确认
UploadFile方法的参数结构和命名空间是否正确。
内容的提问来源于stack exchange,提问作者Nkmelnikov
相关产品推荐
相关产品推荐

