Formattatore di richieste SOAP

Formatta e controlla le envelope SOAP. Le credenziali restano qui.

Input
Output
In attesaIncolla un documento per controllarlo. La convalida gira mentre scrivi.

Tutto viene eseguito in questa scheda. Nulla di ciò che incolli viene caricato, registrato o inviato da qualche parte. Apri il pannello di rete e verifica.

Incolla qui sopra una richiesta o una risposta SOAP e viene indentata mentre scrivi, con i prefissi di namespace attenuati così che soap:Body si legga come Body con un contrassegno accanto, e ogni problema di buona formazione segnalato con la sua riga, la sua colonna e cosa scrivere al suo posto. Porta il controllo degli attributi a tre e l’Envelope smette di essere una riga da 300 caratteri: ogni dichiarazione xmlns ottiene una riga propria.

La busta che stai incollando è quasi certamente uscita da un log: una cattura di Fiddler, un dump di __getLastRequest() del SoapClient di PHP, una traccia dei messaggi di WCF, una scheda raw di SoapUI, o una riga scritta alle 3 di notte da un logger che non va a capo. In quello stato è illeggibile, e ti serve leggibile prima di poter dire se la colpa è tua o loro.

Nulla viene caricato, e qui è il punto, non una caratteristica da vendere. I payload SOAP trasportano token WS-Security con dentro le password, asserzioni SAML firmate, numeri di conto e cartelle cliniche. Uno dei validatori che si posizionano su queste ricerche ti chiede di spuntare una casella con cui confermi che i tuoi dati vengono conservati sui loro server; un altro sito che pubblica un formattatore SOAP salva pubblicamente i documenti inviati, e Google li ha indicizzati. Qui il parser e il formattatore sono JavaScript in questa scheda.

La busta, e l’URI che identifica una versione

Un messaggio SOAP è un unico documento XML con una forma esterna fissa. La radice è Envelope. Può avere un Header, e in quel caso l’Header viene per primo. Deve avere un Body, che contiene o il payload dell’operazione o un Fault. Tutto ciò che sta sotto Body appartiene al servizio.

La versione è identificata dall’URI del namespace, mai dal prefisso. Il prefisso è arbitrario: soap, soapenv, SOAP-ENV ed env sono tutti in circolazione. Se un server risponde a una richiesta dall’aspetto valido con un fault VersionMismatch, confronta l’URI carattere per carattere prima di guardare qualsiasi altra cosa.

  • SOAP 1.1: namespace http://schemas.xmlsoap.org/soap/envelope/ (la barra finale ne fa parte), Content-Type text/xml, e l’operazione in un’intestazione SOAPAction separata il cui valore deve stare fra apici doppi, eventualmente una coppia vuota.
  • SOAP 1.2: namespace http://www.w3.org/2003/05/soap-envelope, Content-Type application/soap+xml con un parametro action, e nessuna intestazione SOAPAction. Un endpoint 1.2 a cui si dà un content type 1.1 risponde di solito HTTP 415, cosa che fa sembrare il fallimento un problema di trasporto.
  • SOAP 1.1 tollerava elementi dopo il Body. SOAP 1.2 non li tollera: Header e Body sono gli unici figli di Envelope, e il Body viene per ultimo.

Perché gli errori di prefisso sono il guasto SOAP più comune

Le dichiarazioni di namespace vivono sull’elemento Envelope, e la parte che ti interessa sta quattro livelli più in basso. Copia il frammento interessante da un log e ti sei portato i prefissi lasciando indietro le dichiarazioni. Il messaggio allora nomina il prefisso anziché la causa: libxml2 dice «Namespace prefix soap on Body is not defined», .NET dice «'soap' is an undeclared prefix». La scansione qui lo segnala insieme alla dichiarazione da aggiungere.

L’errore inverso è più silenzioso e peggiore. Incollare un frammento senza prefisso in un Body che sta sotto un namespace predefinito sposta ogni suo elemento in quel namespace: il documento si analizza, il servizio lo accetta, e i campi tornano vuoti. Metti xmlns="" sulla radice del frammento incollato per tirartene fuori.

Una regola frega anche le persone esperte: un namespace predefinito si applica ai nomi degli elementi, mai ai nomi degli attributi. Per questo mustUnderstand, actor e role devono portare il prefisso della busta anche quando il namespace della busta è quello predefinito.

mustUnderstand, e le intestazioni che falliscono prima che il payload venga letto

Un blocco di intestazione marcato mustUnderstand è un contratto: un destinatario che interpreta il ruolo indicato deve o capire il blocco o rifiutare l’intero messaggio con un fault MustUnderstand, senza elaborare nulla d’altro. È per questo che una richiesta con un Body perfettamente corretto viene respinta: il servizio non ha mai raggiunto il Body.

Il valore cambia da una versione all’altra, e confonderli è un fallimento silenzioso anziché un errore. SOAP 1.1 definisce i caratteri «1» o «0», con «0» come valore predefinito; SOAP 1.2 lo tipizza come xs:boolean, quindi funzionano anche «true» e «false». Manda mustUnderstand="true" a uno stack 1.1 severo e l’attributo viene letto come assente, rendendo facoltativa un’intestazione obbligatoria. L’indirizzamento è l’altra metà: 1.1 usa actor con un URI, mentre 1.2 lo rinomina role e definisce role/none, role/next e role/ultimateReceiver, quest’ultimo come predefinito. La maggior parte dei fallimenti a questo livello è un’intestazione WS-Security marcata mustUnderstand contro un server che per quell’operazione non ha alcuna politica di sicurezza configurata.

Leggere un soap:Fault

Un Fault è un normale elemento dentro Body, e quando è presente deve essere l’unico figlio di Body. La struttura è cambiata del tutto fra le versioni, ed è per questo che una gestione dei fault scritta per una versione non corrisponde silenziosamente a nulla sull’altra.

In SOAP 1.1 i figli di Fault non sono qualificati: faultcode, faultstring, faultactor e detail stanno in nessun namespace pur essendo Fault nel namespace della busta, quindi //soap:Fault/soap:faultstring non restituisce nulla e deve essere //soap:Fault/faultstring. faultcode contiene un QName, di solito soap:Client (il tuo messaggio era sbagliato) o soap:Server (il loro lato ha fallito, un nuovo tentativo può riuscire).

SOAP 1.2 qualifica e rinomina tutto: Code contiene un Value preso da un elenco fisso (Sender, Receiver, VersionMismatch, MustUnderstand, DataEncodingUnknown) con una catena facoltativa di Subcode, Reason contiene elementi Text che richiedono ciascuno xml:lang, e Node, Role e Detail sostituiscono il resto. Anche lo stato porta informazione: 1.1 restituisce 500 per ogni fault, 1.2 restituisce 400 per Sender e 500 per Receiver.

Inviare e leggere SOAP da codice

La busta che incolli qui di solito è uscita da uno di questi. Ogni esempio invia una richiesta, controlla la presenza di un Fault prima di dare per buono il successo, e analizza la risposta in modo sicuro, dato che è XML proveniente da una controparte e che le impostazioni predefinite di Java, PHP e Python risolvono le entità esterne.

// 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’errore ricorrente in tutti e sei è trattare un HTTP 500 come un fallimento di trasporto. Un fault SOAP 1.1 viene consegnato con stato 500 e una busta completa nel corpo, quindi raise_for_status(), EnsureSuccessStatusCode() e un nudo controllo di response.ok buttano via l’unica descrizione di quel che è andato storto che otterrai.

Domande frequenti

La mia busta contiene una password e i dati di un cliente. Viene caricata?

No. Il parser e il formattatore sono JavaScript che gira in questa scheda, dentro un Web Worker. Non c’è alcun endpoint a cui mandare qualcosa, nessun analytics con accesso all’editor e nessuno script di terze parti.

Verificalo invece di crederci: apri la scheda Rete, incolla la busta e formattala. La pagina carica le proprie risorse una volta e poi tace. Questa verifica conta più qui che in qualunque altro punto del sito, perché un’intestazione WS-Security porta un UsernameToken con un digest della password o, su non pochi servizi interni, la password stessa. Il tuo input resta nel localStorage di questo browser finché non lo cancelli.

Qual è la differenza fra SOAP 1.1 e SOAP 1.2?

Parti dall’URI del namespace, perché tutto il resto ne deriva: 1.1 è http://schemas.xmlsoap.org/soap/envelope/ e 1.2 è http://www.w3.org/2003/05/soap-envelope. Il prefisso non ti dice nulla.

Sul filo, 1.1 usa text/xml con un’intestazione SOAPAction separata e fra apici, e 1.2 usa application/soap+xml con un parametro action e senza SOAPAction. Dentro il messaggio, 1.2 ha riscritto il Fault qualificandone ogni parte, ha tipizzato mustUnderstand come booleano, ha rinominato actor in role e ha vietato elementi applicativi dopo il Body. La maggior parte dei servizi in produzione è ancora 1.1.

Perché continuo a ricevere errori di «prefisso non dichiarato»?

Perché le dichiarazioni xmlns vivono sull’elemento Envelope e tu hai copiato qualcosa più sotto. Un prefisso ha senso solo finché una dichiarazione che lo lega è nello scope, quindi un soap:Body incollato da solo non è nemmeno XML ben formato, figurarsi SOAP valido.

Aggiungi il legame alla radice di ciò che hai incollato: xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" per un frammento 1.1. L’errore inverso non solleva alcun errore, quindi fai attenzione: un frammento senza prefisso incollato in un Body sotto un namespace predefinito viene spostato in silenzio in quel namespace, e i campi tornano vuoti.

Questo convalida la mia busta rispetto allo schema SOAP?

No, e far finta del contrario sarebbe il tipo sbagliato di aiuto. Questa pagina analizza la busta, segnala ogni errore di buona formazione con riga e colonna, controlla che ogni prefisso sia legato, formatta senza toccare i tuoi dati, e riporta dimensione, numero di righe e numero di elementi.

Non impone il modello di contenuto SOAP, quindi non obietterà se Header segue Body, se Body manca, o se hai costruito un fault 1.1 dentro una busta 1.2. Quelli sono vincoli di schema: fai girare la busta contro lo schema della busta SOAP, pubblicato all’URI del namespace, nel validatore XSD di questo sito. Non legge nemmeno il WSDL, non invia richieste e non verifica firme.

Dove trovo la busta grezza da incollare qui?

Dal client piuttosto che dal tuo codice, perché vuoi i byte che sono finiti sul filo, non l’oggetto che hai passato a una libreria. In PHP, costruisci SoapClient con trace e chiama __getLastRequest(). In Java con SAAJ, chiama message.writeTo(System.out) dopo saveChanges(). In .NET, abilita il logging dei messaggi di WCF. In Python con zeep, aggancia l’HistoryPlugin e leggi last_sent.

Da fuori del processo, curl con --data-binary e -D scrive la risposta e le sue intestazioni su file, Fiddler e mitmproxy catturano il traffico dal vivo, e SoapUI ha una scheda raw su entrambi i lati. Comunque tu la ottenga, arriva come un’unica lunga riga, ed è proprio a questo che serve questa pagina.

Strumenti correlati

Approfondimenti

Errori che risolve