Skip to content

Instantly share code, notes, and snippets.

@hkneptune
Created March 27, 2026 04:08
Show Gist options
  • Select an option

  • Save hkneptune/3d5490e758593fdecfb4be863cce4f1d to your computer and use it in GitHub Desktop.

Select an option

Save hkneptune/3d5490e758593fdecfb4be863cce4f1d to your computer and use it in GitHub Desktop.
SOAP Request Samples
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
/// <summary>Holds file attachment metadata for a SOAP request.</summary>
public class AttachmentData
{
public string FilePath { get; set; }
public string ContentType { get; set; }
public string ContentID { get; set; }
}
/// <summary>
/// Sends a SOAP 1.2 request with multiple attachments using MTOM
/// (Message Transmission Optimization Mechanism / XOP packaging).
///
/// MTOM encodes the SOAP envelope as an application/xop+xml MIME part and
/// each binary file as a separate raw-binary MIME part. The SOAP body must
/// reference each attachment with an XOP Include element:
/// &lt;xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include"
/// href="cid:attachment1@example.com"/&gt;
///
/// Wire format:
/// Content-Type: multipart/related; type="application/xop+xml"; boundary="…"
/// --boundary
/// Content-Type: application/xop+xml; charset=UTF-8; type="application/soap+xml"
/// Content-ID: &lt;rootpart&gt;
/// [SOAP 1.2 envelope with &lt;xop:Include&gt; elements]
/// --boundary
/// Content-Type: application/pdf
/// Content-Transfer-Encoding: binary
/// Content-ID: &lt;attachment1@example.com&gt;
/// [raw binary file bytes]
/// --boundary--
/// </summary>
/// <param name="soapXml">SOAP 1.2 envelope with &lt;xop:Include&gt; references in the body.</param>
/// <param name="attachments">Files to attach; each ContentID must match a cid: reference in the XML.</param>
/// <param name="endpointUrl">Target web service URL.</param>
/// <param name="soapAction">SOAPAction / action value for the operation.</param>
/// <returns>Raw response body string (SOAP envelope or MTOM response).</returns>
public static async Task<string> SendMtomSoapRequestAsync(
string soapXml,
IEnumerable<AttachmentData> attachments,
string endpointUrl,
string soapAction)
{
// ── Step 1: Generate a collision-resistant MIME boundary ───────────────────
// The boundary string must not occur anywhere inside the message content.
// A GUID-based prefix makes accidental collisions with binary data negligible.
string boundary = "MtomBoundary_" + Guid.NewGuid().ToString("N");
string rootPartId = "rootpart@mtom.example.com";
// ── Step 2: Build the raw multipart/related body ───────────────────────────
using var bodyStream = new MemoryStream();
// leaveOpen:true keeps bodyStream alive after the StreamWriter is disposed,
// so we can still read/seek the stream before posting it.
using (var writer = new StreamWriter(bodyStream, new UTF8Encoding(false), leaveOpen: true))
{
// ── MIME Part 1: SOAP 1.2 envelope (XOP root part) ────────────────────
// application/xop+xml declares that XOP processing is required.
// The type parameter names the real media type of the logical message
// (application/soap+xml for SOAP 1.2).
await writer.WriteAsync($"--{boundary}\r\n");
await writer.WriteAsync("Content-Type: application/xop+xml; charset=UTF-8; " +
"type=\"application/soap+xml\"\r\n");
await writer.WriteAsync("Content-Transfer-Encoding: 8bit\r\n");
await writer.WriteAsync($"Content-ID: <{rootPartId}>\r\n");
await writer.WriteAsync("\r\n"); // blank line required between headers and body
await writer.WriteAsync(soapXml);
await writer.WriteAsync("\r\n");
await writer.FlushAsync();
// ── MIME Parts 2…N: One part per binary attachment ─────────────────────
foreach (var attachment in attachments)
{
// Headers for this attachment part.
// Content-ID must match the cid: URI inside the corresponding
// <xop:Include href="cid:contentID"/> in the SOAP envelope body above.
await writer.WriteAsync($"--{boundary}\r\n");
await writer.WriteAsync($"Content-Type: {attachment.ContentType}\r\n");
await writer.WriteAsync("Content-Transfer-Encoding: binary\r\n");
await writer.WriteAsync($"Content-ID: <{attachment.ContentID}>\r\n");
await writer.WriteAsync("\r\n");
await writer.FlushAsync();
// Append the raw file bytes. binary transfer encoding means no
// Base64 or quoted-printable transformation is applied, which is
// the key efficiency advantage of MTOM over inline Base64 encoding.
byte[] fileBytes = await File.ReadAllBytesAsync(attachment.FilePath);
await bodyStream.WriteAsync(fileBytes);
await writer.WriteAsync("\r\n");
await writer.FlushAsync();
}
// ── Closing MIME boundary ─────────────────────────────────────────────
await writer.WriteAsync($"--{boundary}--\r\n");
await writer.FlushAsync();
}
bodyStream.Seek(0, SeekOrigin.Begin);
// ── Step 3: Construct the HTTP request with the MTOM Content-Type ─────────
// The outer Content-Type identifies the multipart structure and provides:
// type – media type of the root (XOP) part
// start – Content-ID of the root part (SOAP envelope)
// start-info – actual SOAP media type (for SOAP 1.2 = application/soap+xml)
// boundary – the MIME delimiter string used between parts
using var httpClient = new HttpClient();
var content = new StreamContent(bodyStream);
content.Headers.ContentType = MediaTypeHeaderValue.Parse(
$"multipart/related; " +
$"type=\"application/xop+xml\"; " +
$"start=\"<{rootPartId}>\"; " +
$"start-info=\"application/soap+xml\"; " +
$"boundary=\"{boundary}\"");
// SOAPAction is required by SOAP 1.1 routers; for SOAP 1.2 it is conveyed
// as an action parameter in Content-Type, but adding the header ensures
// compatibility with intermediaries that inspect it regardless of version.
httpClient.DefaultRequestHeaders.Add("SOAPAction", $"\"{soapAction}\"");
// ── Step 4: POST the MTOM message and await the HTTP response ─────────────
HttpResponseMessage httpResponse =
await httpClient.PostAsync(endpointUrl, content);
// Raise an HttpRequestException for any non-2xx HTTP status so the caller
// learns about transport-level failures immediately rather than silently
// receiving a SOAP fault or empty body.
httpResponse.EnsureSuccessStatusCode();
// ── Step 5: Return the response body as a raw string ──────────────────────
// The response may itself be multipart/MTOM. Parse and decode it if
// the service returns XOP-encoded response attachments.
return await httpResponse.Content.ReadAsStringAsync();
}
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.List;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.xml.XMLConstants;
import javax.xml.soap.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/**
* Sends a SOAP 1.2 request with multiple attachments using MTOM
* (Message Transmission Optimization Mechanism / XOP packaging).
*
* MTOM avoids Base64-encoding binary data inline in the XML body.
* Instead, each attachment is sent as a raw binary MIME part and
* referenced from the SOAP body via an <xop:Include href="cid:…"/>
* element. This significantly reduces message size for large files.
*
* The soapXml parameter must already contain the XOP placeholders, e.g.:
* <FileData>
* <xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include"
* href="cid:attachment1@example.com"/>
* </FileData>
*
* @param soapXml SOAP 1.2 envelope XML with <xop:Include> references
* @param attachments List of files to attach (path, MIME type, content-ID)
* @param endpointUrl Target SOAP service URL
* @return SOAP response envelope as a String
*/
public static String sendMtomSoapRequest(
String soapXml,
List<AttachmentData> attachments,
String endpointUrl) throws Exception {
// ── Step 1: Create a SOAP 1.2 MessageFactory ──────────────────────────────
// MTOM is only defined for SOAP 1.2. SOAP_1_2_PROTOCOL selects the correct
// factory that uses application/soap+xml as the inner envelope content type,
// which MTOM wraps with application/xop+xml.
MessageFactory messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);
// ── Step 2: Parse the SOAP envelope XML into a SOAPMessage ────────────────
// The XML string is converted to a byte stream so the factory can parse it.
// Each <xop:Include href="cid:…"/> in the body acts as a pointer to the
// corresponding binary MIME part that will be added in Step 4.
InputStream xmlStream = new ByteArrayInputStream(soapXml.getBytes("UTF-8"));
SOAPMessage soapMessage = messageFactory.createMessage(null, xmlStream);
// ── Step 3: Set the MTOM/XOP Content-Type on the primary MIME part ────────
// application/xop+xml with type="application/soap+xml" tells the receiver
// to apply XOP processing: it must find and dereference every cid: URI in
// the envelope by looking up the matching attachment MIME part.
MimeHeaders mimeHeaders = soapMessage.getMimeHeaders();
mimeHeaders.setHeader("Content-Type",
"application/xop+xml; charset=UTF-8; type=\"application/soap+xml\"");
// ── Step 4: Add each file as a separate binary XOP MIME part ──────────────
for (AttachmentData data : attachments) {
// DataHandler + FileDataSource wraps the file path in a clean I/O
// abstraction and ensures the SAAJ implementation reads the file lazily.
// This avoids leaving open FileInputStream handles that could leak.
DataHandler dataHandler = new DataHandler(new FileDataSource(data.FilePath));
AttachmentPart attachment = soapMessage.createAttachmentPart(dataHandler);
// The Content-ID (angle-bracket form) must exactly match the cid: URI
// in the <xop:Include href="cid:…"/> placeholder in the SOAP body.
// Without this match the receiver cannot reassemble the logical message.
attachment.setContentId("<" + data.ContentID + ">");
attachment.setContentType(data.ConcentType);
// binary transfer encoding means the bytes are sent as-is with no
// additional Base64 or quoted-printable transformation at the MIME level,
// which is the whole performance benefit of MTOM over SOAP with Attachments.
attachment.addMimeHeader("Content-Transfer-Encoding", "binary");
soapMessage.addAttachmentPart(attachment);
}
// ── Step 5: Finalise MIME boundaries and envelope headers ─────────────────
// saveChanges() recomputes the multipart MIME structure, assigns boundary
// separators between parts, and updates Content-Length where applicable.
// It must be called before transmitting the message.
soapMessage.saveChanges();
// ── Step 6: Open a SOAP connection and dispatch the request ───────────────
SOAPConnectionFactory connFactory = SOAPConnectionFactory.newInstance();
SOAPConnection connection = connFactory.createConnection();
SOAPMessage soapResponse;
try {
soapResponse = connection.call(soapMessage, endpointUrl);
} finally {
// Always close the connection in a finally block so that the underlying
// HTTP socket is released even if the remote call throws an exception.
connection.close();
}
// ── Step 7: Serialize the response SOAP envelope to a String ──────────────
org.w3c.dom.Document responseDoc =
soapResponse.getSOAPPart().getEnvelope().getOwnerDocument();
TransformerFactory tf = TransformerFactory.newInstance();
// Disable external DTD and stylesheet loading to prevent XXE injection
// attacks (OWASP A03:2021 – Injection / XML External Entity processing).
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
StringWriter output = new StringWriter();
tf.newTransformer().transform(new DOMSource(responseDoc), new StreamResult(output));
return output.toString();
}
/// <summary>
/// Sends a SOAP 1.1 request with multiple attachments using SwA
/// (SOAP with Attachments, W3C Note, 2000).
///
/// Unlike MTOM, SwA uses plain text/xml for the SOAP envelope and references
/// each binary attachment from the SOAP body with a href="cid:…" attribute.
/// There are no XOP Include elements. SwA is the legacy attachment mechanism
/// for SOAP 1.1 systems that predate or do not support MTOM/XOP.
///
/// Wire format:
/// Content-Type: multipart/related; type="text/xml"; boundary="…"
/// --boundary
/// Content-Type: text/xml; charset=UTF-8
/// Content-ID: &lt;soaproot&gt;
/// [SOAP 1.1 envelope — body elements carry href="cid:…" attributes]
/// --boundary
/// Content-Type: application/pdf
/// Content-Transfer-Encoding: binary
/// Content-ID: &lt;attachment1@example.com&gt;
/// Content-Disposition: attachment; filename="invoice.pdf"
/// [raw binary file bytes]
/// --boundary--
/// </summary>
/// <param name="soapXml">SOAP 1.1 envelope; body elements reference attachments via href="cid:…".</param>
/// <param name="attachments">Files to attach; each ContentID must match a cid: in the XML body.</param>
/// <param name="endpointUrl">Target web service URL.</param>
/// <param name="soapAction">SOAPAction header value identifying the SOAP operation.</param>
/// <returns>Raw response body string.</returns>
public static async Task<string> SendSwaSoapRequestAsync(
string soapXml,
IEnumerable<AttachmentData> attachments,
string endpointUrl,
string soapAction)
{
// ── Step 1: Generate a unique MIME boundary ────────────────────────────────
// The boundary appears as a line prefix to delimit MIME parts.
// Using a GUID prevents any accidental collision with binary content.
string boundary = "SwABoundary_" + Guid.NewGuid().ToString("N");
string rootPartId = "soaproot@swa.example.com";
// ── Step 2: Compose the multipart/related message body ────────────────────
using var bodyStream = new MemoryStream();
using (var writer = new StreamWriter(bodyStream, new UTF8Encoding(false), leaveOpen: true))
{
// ── MIME Part 1: SOAP 1.1 XML envelope (primary part) ─────────────────
// The Content-Type here is text/xml (not application/xop+xml) because
// SwA does not use XOP. This is the key structural difference from MTOM:
// the XML body is sent as regular text without any XOP optimisation.
await writer.WriteAsync($"--{boundary}\r\n");
await writer.WriteAsync("Content-Type: text/xml; charset=UTF-8\r\n");
await writer.WriteAsync("Content-Transfer-Encoding: 8bit\r\n");
await writer.WriteAsync($"Content-ID: <{rootPartId}>\r\n");
await writer.WriteAsync("\r\n"); // blank line separates headers from body
await writer.WriteAsync(soapXml);
await writer.WriteAsync("\r\n");
await writer.FlushAsync();
// ── MIME Parts 2…N: Binary file attachments ────────────────────────────
foreach (var attachment in attachments)
{
// Each attachment is identified by a Content-ID that must match,
// without angle brackets, the cid: value in the SOAP body attribute
// e.g. <ns:Document href="cid:invoice.pdf@example.com"/>
await writer.WriteAsync($"--{boundary}\r\n");
await writer.WriteAsync($"Content-Type: {attachment.ContentType}\r\n");
await writer.WriteAsync("Content-Transfer-Encoding: binary\r\n");
await writer.WriteAsync($"Content-ID: <{attachment.ContentID}>\r\n");
// Content-Disposition is optional but recommended; it provides a
// filename hint for recipient agents that save or display attachments.
string fileName = Path.GetFileName(attachment.FilePath);
await writer.WriteAsync(
$"Content-Disposition: attachment; filename=\"{fileName}\"\r\n");
await writer.WriteAsync("\r\n");
await writer.FlushAsync();
// Write the raw file bytes into the body stream.
// binary transfer encoding preserves the bytes exactly as-is without
// any Base64 or quoted-printable transformation at the MIME level.
byte[] fileBytes = await File.ReadAllBytesAsync(attachment.FilePath);
await bodyStream.WriteAsync(fileBytes);
await writer.WriteAsync("\r\n");
await writer.FlushAsync();
}
// ── Closing MIME boundary ─────────────────────────────────────────────
await writer.WriteAsync($"--{boundary}--\r\n");
await writer.FlushAsync();
}
bodyStream.Seek(0, SeekOrigin.Begin);
// ── Step 3: Attach the correct Content-Type for a SwA message ─────────────
// For SwA the outer Content-Type is multipart/related with type="text/xml"
// (matching the primary SOAP part type, unlike MTOM which uses xop+xml).
// The start parameter names the root part by its Content-ID.
using var httpClient = new HttpClient();
var content = new StreamContent(bodyStream);
content.Headers.ContentType = MediaTypeHeaderValue.Parse(
$"multipart/related; " +
$"type=\"text/xml\"; " +
$"start=\"<{rootPartId}>\"; " +
$"boundary=\"{boundary}\"");
// SOAPAction is mandatory in SOAP 1.1 (spec section 6.1.1).
// The value must be quoted and should identify the target SOAP operation URI.
httpClient.DefaultRequestHeaders.Add("SOAPAction", $"\"{soapAction}\"");
// ── Step 4: POST the SwA request ──────────────────────────────────────────
HttpResponseMessage httpResponse =
await httpClient.PostAsync(endpointUrl, content);
// Throw for non-2xx so the caller is not silently handed an error page.
httpResponse.EnsureSuccessStatusCode();
// ── Step 5: Return the response as a string ────────────────────────────────
return await httpResponse.Content.ReadAsStringAsync();
}
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.StringWriter;
import java.util.List;
import javax.xml.XMLConstants;
import javax.xml.soap.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/**
* Sends a SOAP 1.1 request with multiple attachments using SwA
* (SOAP with Attachments, W3C Note, 2000).
*
* Unlike MTOM, SwA does not use XOP. The SOAP body references each attachment
* via a plain href="cid:…" attribute, and each file is attached as a separate
* MIME part in a multipart/related message. SwA is the older standard and is
* supported by a wider range of legacy SOAP 1.1 services.
*
* Example SOAP body reference:
* <ns:Document href="cid:invoice.pdf"/>
*
* @param soapXml SOAP 1.1 envelope XML with href="cid:…" references
* @param attachments List of files to attach (path, MIME type, content-ID)
* @param endpointUrl Target SOAP service URL
* @return SOAP response envelope as a String
*/
public static String sendSwaSoapRequest(
String soapXml,
List<AttachmentData> attachments,
String endpointUrl) throws Exception {
// ── Step 1: Create a SOAP 1.1 MessageFactory ──────────────────────────────
// SwA uses SOAP 1.1 (text/xml content type). SOAP_1_1_PROTOCOL guarantees
// the correct envelope namespace and Content-Type for SOAP 1.1 services.
MessageFactory messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_1_PROTOCOL);
// ── Step 2: Parse the SOAP XML into a SOAPMessage ─────────────────────────
// The SOAP body elements that correspond to attachments must carry the
// attribute href="cid:contentID". The contentID must equal the Content-ID
// set on the matching MIME attachment part (without angle brackets).
InputStream xmlStream = new ByteArrayInputStream(soapXml.getBytes("UTF-8"));
SOAPMessage soapMessage = messageFactory.createMessage(null, xmlStream);
// ── Step 3: Add each file as a raw binary MIME attachment part ────────────
for (AttachmentData data : attachments) {
AttachmentPart attachment = soapMessage.createAttachmentPart();
// try-with-resources ensures the FileInputStream is closed immediately
// after SAAJ copies the bytes into its internal buffer.
// This prevents file handle leaks even if an exception is thrown.
try (FileInputStream fileStream = new FileInputStream(data.FilePath)) {
// setRawContent reads the stream and stores the bytes associated
// with the given MIME type (e.g. "application/pdf", "image/jpeg").
attachment.setRawContent(fileStream, data.ConcentType);
}
// Content-ID in angle brackets per RFC 2392. The cid: URI referenced in
// href="cid:…" in the SOAP body must equal this ID without the angle brackets.
attachment.setContentId("<" + data.ContentID + ">");
// Content-Disposition provides a human-readable filename for clients that
// save or display the attachment parts individually.
attachment.addMimeHeader("Content-Disposition",
"attachment; filename=\"" + new File(data.FilePath).getName() + "\"");
soapMessage.addAttachmentPart(attachment);
}
// ── Step 4: Finalise the MIME multipart/related message structure ──────────
// saveChanges() promotes the message Content-Type to multipart/related,
// assigns MIME boundary delimiters between parts, and recalculates headers.
// Without this call the message may be sent with an incorrect Content-Type.
soapMessage.saveChanges();
// ── Step 5: Send the request over a SOAP connection ───────────────────────
SOAPConnectionFactory connFactory = SOAPConnectionFactory.newInstance();
SOAPConnection connection = connFactory.createConnection();
SOAPMessage soapResponse;
try {
soapResponse = connection.call(soapMessage, endpointUrl);
} finally {
// Release the connection unconditionally to avoid socket leaks in error paths.
connection.close();
}
// ── Step 6: Convert the SOAP response envelope to a String ────────────────
org.w3c.dom.Document responseDoc =
soapResponse.getSOAPPart().getEnvelope().getOwnerDocument();
TransformerFactory tf = TransformerFactory.newInstance();
// Prevent XXE attacks by blocking all external DTD and stylesheet resolution.
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
StringWriter output = new StringWriter();
tf.newTransformer().transform(new DOMSource(responseDoc), new StreamResult(output));
return output.toString();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment