SOAP-Request-Formatierer

Formatiert und prüft SOAP-Envelopes. Zugangsdaten bleiben hier.

Eingabe
Ausgabe
WartetFügen Sie ein Dokument ein. Die Prüfung läuft während der Eingabe.

Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.

Fügen Sie oben eine SOAP-Anfrage oder -Antwort ein, und sie wird beim Tippen eingerückt – die Namensraumpräfixe gedimmt, sodass soap:Body als Body mit einer angehängten Markierung zu lesen ist, und jedes Wohlgeformtheitsproblem mit Zeile, Spalte und dem, was stattdessen dasteht, gemeldet. Stellen Sie die Attributsteuerung auf drei, und der Envelope ist keine Zeile mit 300 Zeichen mehr: Jede xmlns-Deklaration bekommt ihre eigene Zeile.

Der Envelope, den Sie einfügen, kam fast sicher aus einem Log: ein Mitschnitt aus Fiddler, eine Ausgabe von __getLastRequest() des PHP-SoapClient, ein WCF-Message-Trace, ein Raw-Tab aus SoapUI, oder eine Zeile, die um 3 Uhr nachts ein Logger geschrieben hat, der nicht umbricht. In diesem Zustand ist er unlesbar, und Sie brauchen ihn lesbar, bevor Sie sagen können, ob der Fehler bei Ihnen oder bei denen liegt.

Nichts wird hochgeladen, und hier ist das der Punkt und kein Werbeargument. SOAP-Payloads tragen WS-Security-Tokens mit Passwörtern darin, signierte SAML-Assertions, Kontonummern und Patientenakten. Einer der Validatoren, die zu diesen Suchen ranken, bittet Sie, ein Kästchen anzukreuzen, mit dem Sie bestätigen, dass Ihre Daten auf seinen Servern gespeichert werden; eine andere Seite, die einen SOAP-Formatter veröffentlicht, speichert eingesandte Dokumente öffentlich, und Google hat sie indexiert. Hier sind Parser und Formatter JavaScript in diesem Tab.

Der Envelope und die URI, die eine Version kennzeichnet

Eine SOAP-Nachricht ist ein XML-Dokument mit fester äußerer Form. Die Wurzel ist Envelope. Sie darf einen Header haben, und wenn ja, kommt der Header zuerst. Sie muss einen Body haben, der entweder die Nutzlast der Operation oder einen Fault enthält. Alles unterhalb von Body gehört dem Dienst.

Die Version wird von der Namensraum-URI gekennzeichnet, nie vom Präfix. Das Präfix ist beliebig: soap, soapenv, SOAP-ENV und env sind alle im Umlauf. Antwortet ein Server auf eine gültig aussehende Anfrage mit einem VersionMismatch-Fault, vergleichen Sie die URI Zeichen für Zeichen, bevor Sie irgendetwas anderes ansehen.

  • SOAP 1.1: Namensraum http://schemas.xmlsoap.org/soap/envelope/ (der abschließende Schrägstrich gehört dazu), Content-Type text/xml, und die Operation in einem eigenen SOAPAction-Header, dessen Wert in Anführungszeichen stehen muss – notfalls als leeres Anführungszeichenpaar.
  • SOAP 1.2: Namensraum http://www.w3.org/2003/05/soap-envelope, Content-Type application/soap+xml mit einem action-Parameter, und kein SOAPAction-Header. Ein 1.2-Endpunkt, dem man einen 1.1-Content-Type gibt, antwortet meist mit HTTP 415, was den Fehlschlag wie ein Transportproblem aussehen lässt.
  • SOAP 1.1 duldete Elemente nach dem Body. SOAP 1.2 nicht: Header und Body sind die einzigen Kinder von Envelope, und Body steht zuletzt.

Warum Präfixfehler der häufigste SOAP-Defekt sind

Namensraumdeklarationen stehen am Envelope-Element, und der Teil, der Sie interessiert, liegt vier Ebenen darunter. Kopieren Sie das interessante Fragment aus einem Log, haben Sie die Präfixe mitgenommen und die Deklarationen zurückgelassen. Die Meldung nennt dann das Präfix statt der Ursache: libxml2 sagt „Namespace prefix soap on Body is not defined“, .NET sagt „'soap' is an undeclared prefix“. Die Prüfung hier meldet es samt der Deklaration, die fehlt.

Der umgekehrte Fehler ist leiser und schlimmer. Fügt man ein präfixloses Fragment in einen Body ein, der unter einem Standardnamensraum liegt, wandert jedes Element darin in diesen Namensraum: Das Dokument parst, der Dienst nimmt es an, und die Felder kommen leer zurück. Setzen Sie xmlns="" an die Wurzel des eingefügten Fragments, um auszusteigen.

Eine Regel erwischt auch erfahrene Leute: Ein Standardnamensraum gilt für Elementnamen, nie für Attributnamen. Darum müssen mustUnderstand, actor und role das Envelope-Präfix tragen, selbst wenn der Envelope-Namensraum der Standard ist.

mustUnderstand, und Header, die scheitern, bevor Ihre Nutzlast gelesen wird

Ein mit mustUnderstand markierter Header-Block ist ein Vertrag: Ein Empfänger, der die angesprochene Rolle spielt, muss den Block entweder verstehen oder die ganze Nachricht mit einem MustUnderstand-Fault abweisen und sonst nichts verarbeiten. Darum wird eine Anfrage mit völlig korrektem Body abgelehnt; der Dienst hat den Body nie erreicht.

Der Wert unterscheidet sich je Version, und sie zu verwechseln ist ein stiller Fehlschlag statt eines Fehlers. SOAP 1.1 definiert die Zeichen „1“ oder „0“, Vorgabe „0“; SOAP 1.2 typisiert es als xs:boolean, also funktionieren auch „true“ und „false“. Senden Sie mustUnderstand="true" an einen strengen 1.1-Stack, und das Attribut wird als fehlend gelesen, womit ein verpflichtender Header optional wird. Die Adressierung ist die andere Hälfte: 1.1 benutzt actor mit einer URI, 1.2 benennt es in role um und definiert role/none, role/next und role/ultimateReceiver, letzteres als Vorgabe. Die meisten Fehlschläge auf dieser Schicht sind ein mit mustUnderstand markierter WS-Security-Header gegen einen Server, für den zu dieser Operation keine Sicherheitsrichtlinie konfiguriert ist.

Einen soap:Fault lesen

Ein Fault ist ein gewöhnliches Element innerhalb von Body, und wenn er da ist, muss er das einzige Kind von Body sein. Die Struktur hat sich zwischen den Versionen vollständig geändert, und darum passt eine gegen eine Version geschriebene Fault-Behandlung gegen die andere still auf nichts.

In SOAP 1.1 sind die Kinder von Fault unqualifiziert: faultcode, faultstring, faultactor und detail liegen in keinem Namensraum, obwohl Fault im Envelope-Namensraum liegt, also liefert //soap:Fault/soap:faultstring nichts und es muss //soap:Fault/faultstring heißen. faultcode enthält einen QName, meist soap:Client (Ihre Nachricht war falsch) oder soap:Server (deren Seite ist gescheitert, ein neuer Versuch kann klappen).

SOAP 1.2 qualifiziert und benennt alles um: Code enthält ein Value aus einer festen Liste (Sender, Receiver, VersionMismatch, MustUnderstand, DataEncodingUnknown) mit einer optionalen Subcode-Kette, Reason enthält Text-Elemente, die jeweils xml:lang brauchen, und Node, Role und Detail ersetzen den Rest. Der Status trägt ebenfalls Information: 1.1 gibt für jeden Fault 500 zurück, 1.2 gibt 400 für Sender und 500 für Receiver.

SOAP im Code senden und lesen

Der Envelope, den Sie hier einfügen, kam meist aus einem dieser Wege. Jedes Beispiel sendet eine Anfrage, prüft auf einen Fault, bevor es Erfolg annimmt, und parst die Antwort sicher, denn sie ist XML von einer Gegenstelle, und die Voreinstellungen in Java, PHP und Python lösen externe Entities auf.

// SOAP 1.1 over fetch. Note SOAPAction: it is a separate header and its
// value must be quoted, even when it is empty.
const envelope = [
  '<?xml version="1.0" encoding="UTF-8"?>',
  '<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"',
  '               xmlns:ns="urn:example:orders">',
  '  <soap:Body>',
  '    <ns:GetOrder><ns:id>ORD-4471</ns:id></ns:GetOrder>',
  '  </soap:Body>',
  '</soap:Envelope>',
].join('\n');

const response = await fetch('https://example.com/orders', {
  method: 'POST',
  headers: {
    'Content-Type': 'text/xml; charset=utf-8',
    SOAPAction: '"urn:example:orders/GetOrder"',
    // SOAP 1.2 instead: no SOAPAction header, and
    // 'Content-Type': 'application/soap+xml; charset=utf-8; action="urn:example:orders/GetOrder"'
  },
  body: envelope,
});

// A fault arrives with HTTP 500 in SOAP 1.1, so response.ok is false and the
// body still holds the answer. Never throw on the status alone.
const text = await response.text();
const doc = new DOMParser().parseFromString(text, 'application/xml');
const SOAP11 = 'http://schemas.xmlsoap.org/soap/envelope/';
const fault = doc.getElementsByTagNameNS(SOAP11, 'Fault')[0];
if (fault) {
  // faultcode and faultstring are unqualified, even inside a qualified Fault.
  const code = fault.getElementsByTagName('faultcode')[0]?.textContent;
  const reason = fault.getElementsByTagName('faultstring')[0]?.textContent;
  throw new Error(code + ': ' + reason);
}
import requests
from defusedxml.ElementTree import fromstring   # never the stdlib parser here

SOAP11 = 'http://schemas.xmlsoap.org/soap/envelope/'
NS = {'soap': SOAP11, 'ns': 'urn:example:orders'}

envelope = """<?xml version="1.0" encoding="UTF-8"?>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
               xmlns:ns="urn:example:orders">
  <soap:Body>
    <ns:GetOrder><ns:id>ORD-4471</ns:id></ns:GetOrder>
  </soap:Body>
</soap:Envelope>"""

response = requests.post(
    'https://example.com/orders',
    data=envelope.encode('utf-8'),
    headers={
        'Content-Type': 'text/xml; charset=utf-8',
        'SOAPAction': '"urn:example:orders/GetOrder"',
    },
    timeout=30,
)

# Do not call raise_for_status(): a SOAP 1.1 fault is HTTP 500 and the body
# is the part you need.
root = fromstring(response.content)
fault = root.find('.//soap:Fault', NS)
if fault is not None:
    code = fault.findtext('faultcode')      # unqualified in SOAP 1.1
    reason = fault.findtext('faultstring')
    raise RuntimeError(f'{code}: {reason}')

# For a real client, zeep reads the WSDL and builds the envelope for you.
# This shape is for debugging one call, which is when you end up here.
import jakarta.xml.soap.*;   // javax.xml.soap before Jakarta EE 9
import java.io.ByteArrayOutputStream;

// SOAPConstants.SOAP_1_2_PROTOCOL for a 1.2 endpoint. The choice sets both
// the envelope namespace and the content type, so it is the one line that
// decides which version you are speaking.
MessageFactory factory = MessageFactory.newInstance(SOAPConstants.SOAP_1_1_PROTOCOL);
SOAPMessage message = factory.createMessage();

SOAPEnvelope envelope = message.getSOAPPart().getEnvelope();
envelope.addNamespaceDeclaration("ns", "urn:example:orders");

SOAPBody body = envelope.getBody();
SOAPElement call = body.addChildElement("GetOrder", "ns");
call.addChildElement("id", "ns").addTextNode("ORD-4471");

// SOAPAction, quoted, as a MIME header. SOAP 1.2 does not use it.
message.getMimeHeaders().addHeader("SOAPAction", "\"urn:example:orders/GetOrder\"");
message.saveChanges();

// The raw bytes on the wire: this is what you paste into a formatter.
ByteArrayOutputStream sent = new ByteArrayOutputStream();
message.writeTo(sent);
System.out.println(sent.toString("UTF-8"));

SOAPConnection connection = SOAPConnectionFactory.newInstance().createConnection();
SOAPMessage response = connection.call(message, "https://example.com/orders");

if (response.getSOAPBody().hasFault()) {
    SOAPFault fault = response.getSOAPBody().getFault();
    throw new RuntimeException(
        fault.getFaultCode() + ": " + fault.getFaultString());
}
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Xml;
using System.Xml.Linq;

const string Soap11 = "http://schemas.xmlsoap.org/soap/envelope/";
XNamespace soap = Soap11;
XNamespace ns = "urn:example:orders";

var envelope = new XDocument(
    new XElement(soap + "Envelope",
        new XAttribute(XNamespace.Xmlns + "soap", Soap11),
        new XElement(soap + "Body",
            new XElement(ns + "GetOrder",
                new XElement(ns + "id", "ORD-4471")))));

using var http = new HttpClient();
var content = new StringContent(envelope.ToString(), Encoding.UTF8);
content.Headers.ContentType = new MediaTypeHeaderValue("text/xml")
{
    CharSet = "utf-8",
};
// SOAP 1.2 instead: media type application/soap+xml with an action parameter,
// and no SOAPAction header.
content.Headers.Add("SOAPAction", "\"urn:example:orders/GetOrder\"");

var response = await http.PostAsync("https://example.com/orders", content);
var body = await response.Content.ReadAsStringAsync();

// A fault is HTTP 500 with a real body, so do not call
// EnsureSuccessStatusCode() before you have looked at it.
var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,
    XmlResolver = null,
};
using var reader = XmlReader.Create(new StringReader(body), settings);
var doc = XDocument.Load(reader);

var fault = doc.Descendants(soap + "Fault").FirstOrDefault();
if (fault is not null)
{
    // Unqualified children in SOAP 1.1: no namespace on the element name.
    var code = fault.Element("faultcode")?.Value;
    var reason = fault.Element("faultstring")?.Value;
    throw new InvalidOperationException(code + ": " + reason);
}
<?php
// trace => true is why this snippet exists: it is how you get the raw
// envelope to paste into a formatter and see what was actually sent.
$client = new SoapClient('https://example.com/orders?wsdl', [
    'trace'        => true,
    'exceptions'   => true,
    'soap_version' => SOAP_1_1,   // SOAP_1_2 changes the namespace and the
                                  // content type together
    'cache_wsdl'   => WSDL_CACHE_NONE,
    'stream_context' => stream_context_create([
        'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
    ]),
]);

try {
    $result = $client->GetOrder(['id' => 'ORD-4471']);
} catch (SoapFault $e) {
    // faultcode is the QName from the envelope, e.g. "soap:Client".
    fprintf(STDERR, "%s: %s\n", $e->faultcode, $e->getMessage());
} finally {
    // Both are null unless trace was enabled before the call.
    echo $client->__getLastRequest(), "\n";
    echo $client->__getLastResponse(), "\n";
}
# Capture a request and a response you can actually read. The SOAPAction
# value keeps its own quotes inside the header value.
curl -sS -D headers.txt \
  -H 'Content-Type: text/xml; charset=utf-8' \
  -H 'SOAPAction: "urn:example:orders/GetOrder"' \
  --data-binary @request.xml \
  https://example.com/orders \
  | tee response.xml | xmllint --format --nonet -

# SOAP 1.2: no SOAPAction header, the action rides on the content type.
curl -sS \
  -H 'Content-Type: application/soap+xml; charset=utf-8; action="urn:example:orders/GetOrder"' \
  --data-binary @request.xml \
  https://example.com/orders | xmllint --format --nonet -

# Was it a fault? Binding a namespace to xmllint --xpath is awkward, so match
# on the local name:
xmllint --nonet --xpath 'count(//*[local-name()="Fault"])' response.xml

# curl exits 0 on HTTP 500. Check the status line yourself:
head -1 headers.txt

Der wiederkehrende Fehler in allen sechs ist, HTTP 500 als Transportfehler zu behandeln. Ein SOAP-1.1-Fault wird mit Status 500 und einem vollständigen Envelope im Body geliefert, also werfen raise_for_status(), EnsureSuccessStatusCode() und eine blanke response.ok-Prüfung die einzige Beschreibung weg, die Sie über den Fehler bekommen.

Häufige Fragen

Mein Envelope enthält ein Passwort und einen Kundendatensatz. Wird er hochgeladen?

Nein. Parser und Formatter sind JavaScript, das in diesem Tab in einem Web Worker läuft. Es gibt keinen Endpunkt, an den etwas gehen könnte, keine Analytics mit Zugriff auf den Editor und keine Fremdskripte.

Prüfen Sie es, statt es zu glauben: Öffnen Sie den Netzwerk-Tab, fügen Sie den Envelope ein und formatieren Sie. Die Seite lädt ihre eigenen Assets einmal und wird dann still. Diese Prüfung zählt hier mehr als irgendwo sonst auf dieser Seite, denn ein WS-Security-Header trägt ein UsernameToken mit einem Passwort-Digest oder, auf reichlich internen Diensten, das Passwort selbst. Ihre Eingabe bleibt im localStorage dieses Browsers, bis Sie sie löschen.

Was ist der Unterschied zwischen SOAP 1.1 und SOAP 1.2?

Fangen Sie bei der Namensraum-URI an, denn alles andere folgt daraus: 1.1 ist http://schemas.xmlsoap.org/soap/envelope/ und 1.2 ist http://www.w3.org/2003/05/soap-envelope. Das Präfix sagt Ihnen nichts.

Auf der Leitung benutzt 1.1 text/xml mit einem eigenen, in Anführungszeichen gesetzten SOAPAction-Header, und 1.2 benutzt application/soap+xml mit einem action-Parameter und ohne SOAPAction. Innerhalb der Nachricht hat 1.2 den Fault neu geschrieben und jeden seiner Teile qualifiziert, mustUnderstand als Boolean typisiert, actor in role umbenannt und Anwendungselemente nach dem Body verboten. Die meisten Dienste in Produktion sind immer noch 1.1.

Warum bekomme ich ständig Fehler über ein „nicht deklariertes Präfix“?

Weil die xmlns-Deklarationen am Envelope-Element stehen und Sie etwas darunter kopiert haben. Ein Präfix ist nur bedeutsam, solange eine Deklaration, die es bindet, im Gültigkeitsbereich liegt, ein allein eingefügtes soap:Body ist also nicht einmal wohlgeformtes XML, geschweige denn gültiges SOAP.

Fügen Sie die Bindung an die Wurzel dessen, was Sie eingefügt haben: xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" für ein 1.1-Fragment. Der umgekehrte Fehler wirft überhaupt keinen Fehler, also achten Sie darauf: Ein präfixloses Fragment, das in einen Body unter einem Standardnamensraum eingefügt wird, wandert still in diesen Namensraum, und die Felder kommen leer zurück.

Validiert das meinen Envelope gegen das SOAP-Schema?

Nein, und anderes vorzugeben wäre die falsche Art von Hilfsbereitschaft. Diese Seite parst den Envelope, meldet jeden Wohlgeformtheitsfehler mit Zeile und Spalte, prüft, dass jedes Präfix gebunden ist, formatiert ohne Ihre Daten anzutasten, und nennt Größe, Zeilen- und Elementzahl.

Sie erzwingt das SOAP-Inhaltsmodell nicht, wird also nicht widersprechen, wenn Header nach Body kommt, wenn Body fehlt oder wenn Sie einen 1.1-Fault in einem 1.2-Envelope gebaut haben. Das sind Schemabedingungen: Lassen Sie den Envelope im XSD-Validator hier gegen das SOAP-Envelope-Schema laufen, das unter der Namensraum-URI veröffentlicht ist. Sie liest auch kein WSDL, sendet keine Anfragen und prüft keine Signaturen.

Woher bekomme ich den rohen Envelope, den ich hier einfüge?

Vom Client, nicht aus Ihrem Code, denn Sie wollen die Bytes, die auf die Leitung gingen, nicht das Objekt, das Sie einer Bibliothek übergeben haben. In PHP konstruieren Sie SoapClient mit trace und rufen __getLastRequest() auf. In Java mit SAAJ rufen Sie nach saveChanges() message.writeTo(System.out) auf. In .NET schalten Sie das WCF-Message-Logging ein. In Python mit zeep hängen Sie das HistoryPlugin an und lesen last_sent.

Von außerhalb des Prozesses schreibt curl mit --data-binary und -D die Antwort und ihre Header in Dateien, Fiddler und mitmproxy fangen laufenden Verkehr mit, und SoapUI hat auf beiden Seiten einen Raw-Tab. Wie auch immer Sie ihn bekommen: Er kommt als eine lange Zeile, und genau dafür ist diese Seite da.

Verwandte Werkzeuge

Zum Weiterlesen

Fehler, die das behebt