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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 08:12:15