Formateur de requêtes SOAP

Met en forme et vérifie les enveloppes SOAP, sans rien envoyer.

Entrée
Sortie
En attenteCollez un document pour le vérifier. La validation se fait à la frappe.

Tout s'exécute dans cet onglet. Rien de ce que vous collez n'est envoyé, journalisé ni transmis où que ce soit. Ouvrez votre panneau réseau et vérifiez.

Collez ci-dessus une requête ou une réponse SOAP : elle est indentée au fil de la frappe, les préfixes d’espace de noms atténués pour que soap:Body se lise Body avec une marque accolée, et chaque problème de bonne formation signalé avec sa ligne, sa colonne et ce qu’il faut écrire à la place. Mettez le réglage des attributs à trois et l’Envelope cesse d’être une ligne de 300 caractères : chaque déclaration xmlns obtient sa propre ligne.

L’enveloppe que vous collez sort presque à coup sûr d’un journal : une capture de Fiddler, une sortie de __getLastRequest() du SoapClient de PHP, une trace de messages WCF, un onglet raw de SoapUI, ou une ligne écrite à 3 h du matin par un logger qui ne coupe pas les lignes. Dans cet état elle est illisible, et il faut la rendre lisible avant de pouvoir dire si la faute est la vôtre ou la leur.

Rien n’est téléversé, et ici c’est le fond du sujet et non un argument. Les charges SOAP transportent des jetons WS-Security contenant des mots de passe, des assertions SAML signées, des numéros de compte et des dossiers médicaux. L’un des validateurs bien placés sur ces recherches vous demande de cocher une case confirmant que vos données sont stockées sur leurs serveurs ; un autre site qui publie un formateur SOAP enregistre publiquement les documents soumis, et Google les a indexés. Ici l’analyseur et le formateur sont du JavaScript dans cet onglet.

L’enveloppe, et l’URI qui identifie une version

Un message SOAP est un document XML à forme extérieure fixe. La racine est Envelope. Elle peut avoir un Header, et si c’est le cas le Header vient d’abord. Elle doit avoir un Body, contenant soit la charge de l’opération, soit un Fault. Tout ce qui se trouve sous Body appartient au service.

La version est identifiée par l’URI d’espace de noms, jamais par le préfixe. Le préfixe est arbitraire : soap, soapenv, SOAP-ENV et env circulent tous. Si un serveur répond à une requête d’apparence valide par un fault VersionMismatch, comparez l’URI caractère par caractère avant de regarder quoi que ce soit d’autre.

  • SOAP 1.1 : espace de noms http://schemas.xmlsoap.org/soap/envelope/ (la barre oblique finale en fait partie), Content-Type text/xml, et l’opération dans un en-tête SOAPAction distinct dont la valeur doit être entre guillemets, éventuellement une paire de guillemets vide.
  • SOAP 1.2 : espace de noms http://www.w3.org/2003/05/soap-envelope, Content-Type application/soap+xml avec un paramètre action, et pas d’en-tête SOAPAction. Un point d’accès 1.2 recevant un content type 1.1 répond généralement HTTP 415, ce qui fait passer l’échec pour un problème de transport.
  • SOAP 1.1 tolérait des éléments après le Body. SOAP 1.2 ne le tolère pas : Header et Body sont les seuls enfants d’Envelope, et Body vient en dernier.

Pourquoi les erreurs de préfixe sont la panne SOAP la plus courante

Les déclarations d’espace de noms vivent sur l’élément Envelope, et la partie qui vous intéresse se trouve quatre niveaux plus bas. Copiez le fragment intéressant d’un journal et vous avez emporté les préfixes en laissant les déclarations derrière. Le message nomme alors le préfixe plutôt que la cause : libxml2 dit « Namespace prefix soap on Body is not defined », .NET dit « 'soap' is an undeclared prefix ». L’analyse ici le signale avec la déclaration à ajouter.

L’erreur inverse est plus discrète et plus grave. Coller un fragment sans préfixe dans un Body placé sous un espace de noms par défaut déplace chacun de ses éléments dans cet espace : le document s’analyse, le service l’accepte, et les champs reviennent vides. Mettez xmlns="" sur la racine du fragment collé pour vous en extraire.

Une règle piège même les gens expérimentés : un espace de noms par défaut s’applique aux noms d’éléments, jamais aux noms d’attributs. C’est pourquoi mustUnderstand, actor et role doivent porter le préfixe de l’enveloppe même quand l’espace de noms de l’enveloppe est celui par défaut.

mustUnderstand, et les en-têtes qui échouent avant la lecture de votre charge

Un bloc d’en-tête marqué mustUnderstand est un contrat : un destinataire jouant le rôle visé doit soit comprendre le bloc, soit rejeter le message entier par un fault MustUnderstand, sans rien traiter d’autre. C’est pourquoi une requête au Body parfaitement correct se voit refusée ; le service n’a jamais atteint le Body.

La valeur diffère selon la version, et les confondre produit un échec silencieux plutôt qu’une erreur. SOAP 1.1 définit les caractères « 1 » ou « 0 », avec « 0 » par défaut ; SOAP 1.2 le type en xs:boolean, donc « true » et « false » marchent aussi. Envoyez mustUnderstand="true" à une pile 1.1 stricte et l’attribut se lit comme absent, rendant facultatif un en-tête obligatoire. Le ciblage est l’autre moitié : 1.1 utilise actor avec un URI, tandis que 1.2 le renomme role et définit role/none, role/next et role/ultimateReceiver, ce dernier étant la valeur par défaut. La plupart des échecs à cette couche sont un en-tête WS-Security marqué mustUnderstand face à un serveur sans politique de sécurité configurée pour cette opération.

Lire un soap:Fault

Un Fault est un élément ordinaire dans Body, et lorsqu’il est présent il doit être le seul enfant de Body. La structure a complètement changé entre les versions, et c’est pourquoi une gestion des faults écrite pour une version ne correspond silencieusement à rien sur l’autre.

En SOAP 1.1 les enfants de Fault ne sont pas qualifiés : faultcode, faultstring, faultactor et detail sont dans aucun espace de noms alors même que Fault est dans celui de l’enveloppe, donc //soap:Fault/soap:faultstring ne renvoie rien et il faut écrire //soap:Fault/faultstring. faultcode contient un QName, en général soap:Client (votre message était faux) ou soap:Server (leur côté a échoué, un nouvel essai peut passer).

SOAP 1.2 qualifie et renomme tout : Code contient un Value issu d’une liste fixe (Sender, Receiver, VersionMismatch, MustUnderstand, DataEncodingUnknown) avec une chaîne facultative de Subcode, Reason contient des éléments Text qui exigent chacun xml:lang, et Node, Role et Detail remplacent le reste. Le statut porte aussi de l’information : 1.1 renvoie 500 pour tout fault, 1.2 renvoie 400 pour Sender et 500 pour Receiver.

Envoyer et lire du SOAP en code

L’enveloppe que vous collez ici vient généralement de l’un de ces cas. Chaque exemple envoie une requête, vérifie la présence d’un Fault avant de supposer la réussite, et analyse la réponse en sécurité, puisqu’il s’agit de XML venu d’un tiers et que les réglages par défaut de Java, PHP et Python résolvent les entités externes.

// 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

L’erreur récurrente dans les six est de traiter un HTTP 500 comme un échec de transport. Un fault SOAP 1.1 est livré avec le statut 500 et une enveloppe complète dans le corps : raise_for_status(), EnsureSuccessStatusCode() et un simple test de response.ok jettent donc la seule description de ce qui a mal tourné que vous obtiendrez.

Questions fréquentes

Mon enveloppe contient un mot de passe et un dossier client. Est-elle téléversée ?

Non. L’analyseur et le formateur sont du JavaScript exécuté dans cet onglet, dans un Web Worker. Il n’y a aucun point d’accès vers lequel envoyer quoi que ce soit, aucun analytics ayant accès à l’éditeur et aucun script tiers.

Vérifiez-le plutôt que de le croire : ouvrez l’onglet Réseau, collez l’enveloppe et formatez-la. La page charge ses propres ressources une fois, puis se tait. Cette vérification compte plus ici que partout ailleurs sur ce site, car un en-tête WS-Security porte un UsernameToken avec un condensé de mot de passe ou, sur bien des services internes, le mot de passe lui-même. Votre saisie reste dans le localStorage de ce navigateur jusqu’à ce que vous l’effaciez.

Quelle est la différence entre SOAP 1.1 et SOAP 1.2 ?

Commencez par l’URI d’espace de noms, car tout le reste en découle : 1.1 c’est http://schemas.xmlsoap.org/soap/envelope/ et 1.2 c’est http://www.w3.org/2003/05/soap-envelope. Le préfixe ne vous dit rien.

Sur le réseau, 1.1 utilise text/xml avec un en-tête SOAPAction distinct et entre guillemets, et 1.2 utilise application/soap+xml avec un paramètre action et sans SOAPAction. Dans le message, 1.2 a réécrit le Fault et qualifié chacune de ses parties, typé mustUnderstand en booléen, renommé actor en role, et interdit les éléments applicatifs après le Body. La plupart des services en production sont encore en 1.1.

Pourquoi ai-je sans cesse des erreurs de « préfixe non déclaré » ?

Parce que les déclarations xmlns vivent sur l’élément Envelope et que vous avez copié quelque chose en dessous. Un préfixe n’a de sens que tant qu’une déclaration le liant est dans la portée, donc un soap:Body collé seul n’est même pas du XML bien formé, sans parler de SOAP valide.

Ajoutez la liaison à la racine de ce que vous avez collé : xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" pour un fragment 1.1. L’erreur inverse ne lève aucune erreur, alors guettez-la : un fragment sans préfixe collé dans un Body sous un espace de noms par défaut est silencieusement déplacé dans cet espace, et les champs reviennent vides.

Est-ce que cela valide mon enveloppe au regard du schéma SOAP ?

Non, et prétendre le contraire serait une mauvaise façon d’aider. Cette page analyse l’enveloppe, signale chaque erreur de bonne formation avec une ligne et une colonne, vérifie que chaque préfixe est lié, formate sans toucher à vos données, et indique la taille, le nombre de lignes et le nombre d’éléments.

Elle n’impose pas le modèle de contenu SOAP : elle ne protestera donc pas si Header suit Body, si Body manque, ou si vous avez bâti un fault 1.1 dans une enveloppe 1.2. Ce sont des contraintes de schéma : passez l’enveloppe au schéma d’enveloppe SOAP, publié à l’URI d’espace de noms, dans le validateur XSD d’ici. Elle ne lit pas non plus de WSDL, n’envoie pas de requêtes et ne vérifie pas de signatures.

Où récupérer l’enveloppe brute à coller ici ?

Du client plutôt que de votre code, car vous voulez les octets partis sur le réseau, pas l’objet remis à une bibliothèque. En PHP, construisez SoapClient avec trace et appelez __getLastRequest(). En Java avec SAAJ, appelez message.writeTo(System.out) après saveChanges(). En .NET, activez la journalisation des messages WCF. En Python avec zeep, branchez le HistoryPlugin et lisez last_sent.

Depuis l’extérieur du processus, curl avec --data-binary et -D écrit la réponse et ses en-têtes dans des fichiers, Fiddler et mitmproxy capturent le trafic en direct, et SoapUI a un onglet raw des deux côtés. Quelle que soit la manière, elle arrive en une seule longue ligne : c’est précisément à cela que sert cette page.

Outils associés

Pour aller plus loin

Erreurs que cet outil résout