PAYATHON 2026

WSO2 MI 转发 base64 文档时偶发 Could not write JSON stream

数据小陈

从现有堆栈可以看出,MI 在向调用方写回响应时发生报错,底层异常是 Consumer error or stale connection has occurred.。仅凭这些日志,无法判断 base64 内容是否有误,也无法确定哪个环节关闭了连接。应先关联同一次请求的完整日志,再检查首次异常、连接超时情况和故障响应路径。

后端收到请求和 MI 返回响应失败可能同时发生。即使后端已经处理成功,调用方也可能因响应传输失败而收不到结果。

堆栈说明了什么

关键调用链是:

RespondMediator.mediate
  -> Axis2Sender.sendBack
  -> PassThroughHttpSender.submitResponse
  -> JsonStreamFormatter.writeTo
  -> JsonUtil.writeJsonStream
  -> Pipe$ByteBufferOutputStream.write
  -> IOException: Consumer error or stale connection has occurred.

Could not write JSON stream 是上层包装的错误。最深层异常表明传输管道写入失败,没有直接给出 JSON 语法错误或 base64 解码失败的证据。

堆栈中还出现了 MediatorFaultHandler.onFault,说明这次响应写入发生在故障处理路径中。当前的发送失败可能是处理先前异常时产生的后续错误,因此需要继续查找进入故障处理之前的第一条异常。

日志中的 [ENDED] 位于 <respond /> 之前,只能说明执行到了这条日志,不能证明响应已经完整送达调用方。

先把同一次请求的日志串起来

现有日志中,写入失败和稍后的默认故障日志带有相同的 MessageID,可以关联。最后两条 Http code = 201 和业务成功日志没有显示请求标识,不能仅凭时间相近就认定它们属于同一次调用。

建议分别在进入接口、调用后端之前、后端返回之后和故障处理入口记录同一个请求标识。例如:

<log level="custom">
    <property name="stage" value="after-backend-call" />
    <property name="MessageID" expression="get-property('MessageID')" />
    <property name="HTTP_SC" expression="$axis2:HTTP_SC" />
</log>

在故障处理入口、调用 defaultFaultSequence 之前,记录当时的错误属性:

<log level="custom">
    <property name="stage" value="fault-entry" />
    <property name="MessageID" expression="get-property('MessageID')" />
    <property name="ERROR_CODE" expression="get-property('ERROR_CODE')" />
    <property name="ERROR_MESSAGE" expression="get-property('ERROR_MESSAGE')" />
</log>

结合 MI、后端和入口代理的日志,确认请求开始时间、后端实际完成时间、首次异常时间,以及调用方何时结束等待。050 和对应文案的设置位置也需要查明。现有材料无法判断它们来自哪个自定义序列,不能直接将其解释为这次传输异常的标准错误码。

检查故障处理是否只有一个响应出口

当前故障流程先执行:

<sequence key="defaultFaultSequence" />

随后构造 JSON 并执行 <respond />。需要查看 defaultFaultSequence 的实际内容,确认它是否已经发送响应、终止处理,或者改写了错误属性及消息体。

如果被调用序列已经负责发送响应,应统一响应出口;如果它只记录日志、设置属性,外层继续构造响应可以有合理用途。仅凭序列名称无法判断是否发生了重复响应。

另一个需要修正的配置是:

<property name="HTTP_SC" expression="get-property('HTTP_SC')" />

这里没有指定 scope="axis2",不能据此认为它设置了日志中读取的 $axis2:HTTP_SC。若要明确返回某个错误状态,应显式设置作用域,并根据实际错误选择状态码。例如,对于未分类的内部处理异常:

<property name="HTTP_SC" value="500" scope="axis2" />

这样可以明确错误响应的状态,但无法恢复已经失效的连接。

用简单的故障响应缩小范围

在测试环境中,可以暂时使用固定 JSON 响应来隔离故障处理逻辑。下面的示例用于排查,接入前需确认原故障序列中是否还有必须保留的处理:

<faultSequence>
    <log level="custom">
        <property name="stage" value="fault-entry" />
        <property name="MessageID" expression="get-property('MessageID')" />
        <property name="ERROR_CODE" expression="get-property('ERROR_CODE')" />
        <property name="ERROR_MESSAGE" expression="get-property('ERROR_MESSAGE')" />
    </log>

    <payloadFactory media-type="json">
        <format>{"returnCode":"INTERNAL_ERROR","returnMessage":"Request processing failed"}</format>
        <args />
    </payloadFactory>

    <property name="HTTP_SC" value="500" scope="axis2" />
    <property name="messageType" value="application/json" scope="axis2" />

    <respond />
</faultSequence>

固定响应可以排除动态错误文案引起的 JSON 转义问题,并减少故障路径中的处理步骤。如果仍出现相同的管道写入异常,应进一步检查连接生命周期。对于偶发问题,需要在相近的负载、请求大小和耗时条件下比较,单次成功不能证明问题已经解决。

检查超时和 base64 请求大小的关联

重点核对调用方、入口代理或负载均衡器、MI 和后端之间的超时关系。例如,如果调用方或代理已经结束等待,而后端稍后才返回,MI 写回响应时就可能遇到失效连接。这是需要通过日志验证的可能原因,当前材料尚不能确认。

建议用同一份有效请求做以下对照:

  • 分别测试较小文档和容易出错的文档,记录请求体字节数、总耗时及失败情况。
  • 若链路中存在入口代理,在受控环境中比较经代理与直接访问 MI 的结果。
  • 检查失败是否集中在某个超时边界附近,并与后端处理耗时对应。

base64 会增加传输体积,可能间接影响耗时和资源占用,但这不等于 base64 编码本身有问题。先检查 JSON 是否有效、后端是否按预期接收文档;不要仅凭错误名称修改编码方式。

排查期间,可将完整消息体日志改为请求标识、状态码、耗时和大小等元数据。json-eval($) 会访问消息内容,可能影响流式处理的观察结果;完整文档也不适合持续写入日志。这项调整可以减少干扰,不能直接当作根因修复。

如何判断修复方向

若确认调用方或代理先断开连接,应调整相关超时关系,或缩短后端处理时间;若确认故障序列存在重复响应,应统一响应出口;若发现更早的业务或消息处理异常,应先修复该异常,再复测返回流程。

目前缺少 ApiKeySequence、defaultFaultSequence、stagingPatchService 的配置,以及客户端和代理超时信息,因此还不能给出唯一根因,也没有依据将问题认定为 MI 4.3.0 的特定缺陷。

响应失败时,上传操作仍可能已经在后端完成。重试之前应通过业务标识确认处理结果,并结合接口的幂等设计避免重复上传。

备注:内容仅供参考。