如何从SOAP MTOM/XOP响应中提取附件?
处理SOAP MTOM/XOP响应:提取XML与二进制附件指南
一、先理清MTOM/XOP的核心结构
MTOM本质是MIME多部分消息,把SOAP XML和二进制附件分开存储:
- SOAP XML里通过
xop:Include元素的href属性(格式如cid:xxx@xxx)引用附件的Content-ID - 响应的
Content-Type必然是multipart/related; type="application/xop+xml"; start="<...>"; boundary="...",这是识别MTOM的核心标志
二、C#中完整获取响应的正确姿势
绝对不能直接把响应流转UTF8或Base64——因为多部分MIME混合了文本和二进制内容,必须按MIME边界拆分处理。
具体实现步骤
- 解析Content-Type头,提取边界符
var response = (HttpWebResponse)request.GetResponse(); var contentType = response.ContentType; var boundary = ExtractBoundary(contentType);
- 按边界拆分MIME各部分,分别处理XML和附件
using (var stream = response.GetResponseStream()) using (var reader = new StreamReader(stream)) { var fullContent = reader.ReadToEnd(); var mimeParts = fullContent.Split(new[] { "--" + boundary }, StringSplitOptions.RemoveEmptyEntries); foreach (var part in mimeParts) { // 跳过结尾的终止边界符 if (part.EndsWith("--")) continue; // 拆分每个MIME部分的头和内容体 var headerEndPos = part.IndexOf("\r\n\r\n", StringComparison.Ordinal); if (headerEndPos == -1) continue; var headerSection = part.Substring(0, headerEndPos); var bodySection = part.Substring(headerEndPos + 4); // 跳过头与内容之间的空行 // 解析当前部分的头信息 var headers = ParseMimeHeaders(headerSection); // 区分SOAP XML部分和二进制附件部分 if (headers.TryGetValue("Content-Type", out var ct) && ct.Contains("application/xop+xml")) { // 处理SOAP XML:直接提取内容,后续可解析xop:Include关联附件 var soapXml = bodySection.Trim(); } else { // 处理二进制附件:根据Content-Transfer-Encoding解码 byte[] attachmentBytes; var transferEncoding = headers.GetValueOrDefault("Content-Transfer-Encoding", "").Trim(); if (transferEncoding.Equals("base64", StringComparison.OrdinalIgnoreCase)) { // 去掉Base64内容中的换行符再解码 var cleanBase64 = bodySection.Replace("\r\n", "").Replace("\n", "").Trim(); attachmentBytes = Convert.FromBase64String(cleanBase64); } else { // 若为binary/7bit编码,直接读取字节流(建议用原始流读取更准确) attachmentBytes = Encoding.Default.GetBytes(bodySection); } // 通过Content-ID关联到SOAP XML中的引用 if (headers.TryGetValue("Content-ID", out var cid)) { var cleanCid = cid.Trim('<', '>'); // 去掉CID的尖括号 // 这里可以建立CID与附件字节的映射关系 } } } }
- 辅助方法示例
// 从Content-Type中提取边界符 private static string ExtractBoundary(string contentType) { foreach (var segment in contentType.Split(';')) { var trimmed = segment.Trim(); if (trimmed.StartsWith("boundary=")) { return trimmed.Substring(9).Trim('"'); } } throw new InvalidOperationException("未在Content-Type中找到boundary参数"); } // 解析MIME头为键值对 private static Dictionary<string, string> ParseMimeHeaders(string headerSection) { var headers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); foreach (var line in headerSection.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries)) { var colonIndex = line.IndexOf(':'); if (colonIndex == -1) continue; var key = line.Substring(0, colonIndex).Trim(); var value = line.Substring(colonIndex + 1).Trim(); headers[key] = value; } return headers; }
三、分离XML与附件的关键逻辑
- 定位SOAP XML:优先根据Content-Type的
start参数找到对应CID的MIME部分,这是起始的SOAP内容 - 关联附件:解析SOAP XML中的
xop:Include元素,提取href中的cid:xxx,去掉前缀后与附件的Content-ID(去掉尖括号)匹配,即可关联对应附件字节
四、必须重点关注的响应内容
- Content-Type头:必须包含
multipart/related、type="application/xop+xml"、boundary和start参数,缺一则不是标准MTOM响应 - MIME边界符:拆分时要注意边界符前后的
--(起始边界是--boundary,终止边界是--boundary--) - Content-ID匹配:XML中
xop:Include的href是cid:xxx,而响应头的Content-ID是<xxx>,必须去掉尖括号后再匹配 - Base64编码处理:附件的Base64内容会自动换行(每76字符),解码前必须清除所有换行符,否则会抛出"无效Base64字符串"异常
- 响应流读取:绝对不能用UTF8直接读取整个响应流,二进制附件的字节会被文本编码损坏,必须按MIME部分拆分后分别处理
五、常见踩坑点
- 不要直接将整个响应内容转XML,二进制附件的乱码会破坏XML结构
- 解码Base64时必须清除换行符,MIME规范要求Base64每76字符换行,直接解码会报错
- 注意HTTP头的大小写不敏感,但
Content-ID的尖括号必须正确处理
内容的提问来源于stack exchange,提问作者bikeCoder
相关产品推荐
相关产品推荐

