Formateador de peticiones SOAP
Formatea y revisa envelopes SOAP. Las credenciales no salen de tu pestaña.
Todo se ejecuta en esta pestaña. Nada de lo que pegues se sube, se registra ni se envía a ningún sitio. Abre el panel de red y compruébalo.
Pega arriba una petición o respuesta SOAP y se indenta mientras escribes, con los prefijos de espacio de nombres atenuados para que soap:Body se lea como Body con una marca pegada, y cada problema de buena formación informado con su línea, su columna y qué escribir en su lugar. Pon el control de atributos en tres y el Envelope deja de ser una línea de 300 caracteres: cada declaración xmlns recibe su propio renglón.
El sobre que estás pegando salió casi con certeza de un registro: una captura de Fiddler, un volcado de __getLastRequest() de SoapClient de PHP, un rastreo de mensajes de WCF, una pestaña raw de SoapUI, o una línea escrita a las 03:00 por un logger que no parte líneas. En ese estado es ilegible, y necesitas leerlo antes de poder decir si el fallo es tuyo o suyo.
Nada se sube, y aquí eso es la idea y no una prestación. Las cargas SOAP llevan tokens de WS-Security con contraseñas dentro, aserciones SAML firmadas, números de cuenta e historiales de pacientes. Uno de los validadores que posicionan para estas búsquedas te pide marcar una casilla confirmando que tus datos se guardan en sus servidores; otro sitio que publica un formateador SOAP guarda públicamente los documentos enviados, y Google los ha indexado. Aquí el analizador y el formateador son JavaScript en esta pestaña.
El sobre, y el URI que identifica una versión
Un mensaje SOAP es un documento XML con una forma exterior fija. La raíz es Envelope. Puede tener un Header, y si lo tiene el Header va primero. Debe tener un Body, que contiene o la carga de la operación o un Fault. Todo lo que está por debajo de Body pertenece al servicio.
La versión la identifica el URI del espacio de nombres, nunca el prefijo. El prefijo es arbitrario: soap, soapenv, SOAP-ENV y env están todos en circulación. Si un servidor responde a una petición de aspecto válido con un fault VersionMismatch, compara el URI carácter por carácter antes de mirar cualquier otra cosa.
- SOAP 1.1: espacio de nombres http://schemas.xmlsoap.org/soap/envelope/ (la barra final forma parte de él), Content-Type text/xml, y la operación en una cabecera SOAPAction aparte cuyo valor debe ir entre comillas, posiblemente como un par de comillas vacío.
- SOAP 1.2: espacio de nombres http://www.w3.org/2003/05/soap-envelope, Content-Type application/soap+xml con un parámetro action, y sin cabecera SOAPAction. Un endpoint 1.2 al que le llega un content type de 1.1 suele responder HTTP 415, lo que hace que el fallo parezca un problema de transporte.
- SOAP 1.1 toleraba elementos después del Body. SOAP 1.2 no: Header y Body son los únicos hijos de Envelope, y Body va al final.
Por qué los errores de prefijo son la avería SOAP más común
Las declaraciones de espacio de nombres viven en el elemento Envelope, y la parte que te interesa está cuatro niveles más abajo. Copia el fragmento interesante de un registro y te has llevado los prefijos dejando atrás las declaraciones. El mensaje entonces nombra el prefijo en lugar de la causa: libxml2 dice «Namespace prefix soap on Body is not defined», .NET dice «'soap' is an undeclared prefix». El escaneo de aquí lo informa junto con la declaración que hay que añadir.
El error inverso es más silencioso y peor. Pegar un fragmento sin prefijos en un Body que está bajo un espacio de nombres por defecto mueve cada elemento dentro de él a ese espacio: el documento se analiza, el servicio lo acepta, y los campos vuelven vacíos. Pon xmlns="" en la raíz del fragmento pegado para quedarte fuera.
Una regla pilla incluso a gente con experiencia: un espacio de nombres por defecto se aplica a nombres de elemento, nunca a nombres de atributo. Por eso mustUnderstand, actor y role deben llevar el prefijo del sobre incluso cuando el espacio de nombres del sobre es el predeterminado.
mustUnderstand, y cabeceras que fallan antes de que se lea tu carga
Un bloque de cabecera marcado con mustUnderstand es un contrato: un receptor que juegue el rol apuntado debe o entender el bloque o rechazar el mensaje entero con un fault MustUnderstand, sin procesar nada más. Por eso una petición con un Body perfectamente correcto acaba rechazada; el servicio nunca llegó al Body.
El valor difiere según la versión, y confundirlos es un fallo silencioso y no un error. SOAP 1.1 define los caracteres «1» o «0», con «0» por defecto; SOAP 1.2 lo tipa como xs:boolean, así que «true» y «false» también valen. Envía mustUnderstand="true" a una pila 1.1 estricta y el atributo se lee como ausente, convirtiendo una cabecera obligatoria en opcional. El apuntado es la otra mitad: 1.1 usa actor con un URI, mientras 1.2 lo renombra role y define role/none, role/next y role/ultimateReceiver, siendo este último el predeterminado. La mayoría de los fallos en esta capa son una cabecera WS-Security marcada con mustUnderstand contra un servidor sin política de seguridad configurada para esa operación.
Leer un soap:Fault
Un Fault es un elemento corriente dentro de Body, y cuando está presente debe ser el único hijo de Body. La estructura cambió por completo entre versiones, y por eso un manejo de faults escrito contra una versión no coincide con nada, sin decir nada, contra la otra.
En SOAP 1.1 los hijos de Fault no están cualificados: faultcode, faultstring, faultactor y detail están sin espacio de nombres aunque Fault esté en el del sobre, así que //soap:Fault/soap:faultstring no devuelve nada y tiene que ser //soap:Fault/faultstring. faultcode lleva un QName, normalmente soap:Client (tu mensaje estaba mal) o soap:Server (falló su lado, reintentar puede servir).
SOAP 1.2 cualifica y renombra todo: Code lleva un Value de una lista fija (Sender, Receiver, VersionMismatch, MustUnderstand, DataEncodingUnknown) con una cadena opcional de Subcode, Reason lleva elementos Text que requieren cada uno xml:lang, y Node, Role y Detail sustituyen al resto. El estado también lleva información: 1.1 devuelve 500 para cada fault, 1.2 devuelve 400 para Sender y 500 para Receiver.
Enviar y leer SOAP desde código
El sobre que pegas aquí vino normalmente de uno de estos. Cada ejemplo envía una petición, comprueba si hay un Fault antes de dar por buena la respuesta, y analiza esa respuesta de forma segura, porque es XML de una parte remota y los valores por defecto en Java, PHP y Python resuelven entidades externas.
// 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.txtEl error recurrente en los seis es tratar un HTTP 500 como fallo de transporte. Un fault de SOAP 1.1 se entrega con estado 500 y un sobre completo en el cuerpo, así que raise_for_status(), EnsureSuccessStatusCode() y una comprobación pelada de response.ok tiran la única descripción de lo que salió mal que vas a recibir.
Preguntas frecuentes
Mi sobre contiene una contraseña y un registro de cliente. ¿Se sube?
No. El analizador y el formateador son JavaScript ejecutándose en esta pestaña, en un Web Worker. No hay endpoint al que mandar nada, ni analítica con acceso al editor, ni scripts de terceros.
Compruébalo en lugar de creerlo: abre la pestaña Red, pega el sobre y dale formato. La página carga sus propios recursos una vez y después se calla. Esa comprobación importa aquí más que en cualquier otro sitio de esta web, porque una cabecera WS-Security lleva un UsernameToken con un digest de contraseña o, en muchos servicios internos, la contraseña misma. Tu entrada se queda en el localStorage de este navegador hasta que la borres.
¿Cuál es la diferencia entre SOAP 1.1 y SOAP 1.2?
Empieza por el URI del espacio de nombres, porque todo lo demás se sigue de ahí: 1.1 es http://schemas.xmlsoap.org/soap/envelope/ y 1.2 es http://www.w3.org/2003/05/soap-envelope. El prefijo no te dice nada.
En el cable, 1.1 usa text/xml con una cabecera SOAPAction aparte y entre comillas, y 1.2 usa application/soap+xml con un parámetro action y sin SOAPAction. Dentro del mensaje, 1.2 reescribió el Fault y cualificó todas sus partes, tipó mustUnderstand como booleano, renombró actor a role, y prohibió elementos de aplicación después del Body. La mayoría de los servicios en producción siguen en 1.1.
¿Por qué me salen una y otra vez errores de «prefijo no declarado»?
Porque las declaraciones xmlns viven en el elemento Envelope y copiaste algo por debajo de él. Un prefijo solo tiene significado mientras una declaración que lo vincule esté en ámbito, así que un soap:Body pegado por su cuenta no es XML bien formado, ya no hablemos de SOAP válido.
Añade el vínculo a la raíz de lo que pegaste: xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" para un fragmento 1.1. El error inverso no levanta ningún error, así que estate atento: un fragmento sin prefijos pegado en un Body bajo un espacio de nombres por defecto se mueve en silencio a ese espacio, y los campos vuelven vacíos.
¿Esto valida mi sobre contra el esquema de SOAP?
No, y fingir lo contrario sería el tipo equivocado de ayuda. Esta página analiza el sobre, informa de cada error de buena formación con línea y columna, comprueba que cada prefijo esté vinculado, da formato sin tocar tus datos, e informa del tamaño, el número de líneas y el de elementos.
No impone el modelo de contenido de SOAP, así que no objetará si Header va después de Body, si falta Body, o si montaste un fault 1.1 dentro de un sobre 1.2. Esas son restricciones de esquema: pasa el sobre por el esquema del sobre SOAP, publicado en el URI del espacio de nombres, en el validador de XSD de aquí. Tampoco lee WSDL, ni envía peticiones, ni verifica firmas.
¿De dónde saco el sobre en bruto para pegarlo aquí?
Del cliente antes que de tu código, porque quieres los bytes que fueron por el cable, no el objeto que le pasaste a una biblioteca. En PHP, construye SoapClient con trace y llama a __getLastRequest(). En Java con SAAJ, llama a message.writeTo(System.out) después de saveChanges(). En .NET, activa el registro de mensajes de WCF. En Python con zeep, engancha el HistoryPlugin y lee last_sent.
Desde fuera del proceso, curl con --data-binary y -D escribe la respuesta y sus cabeceras en archivos, Fiddler y mitmproxy capturan tráfico en vivo, y SoapUI tiene una pestaña raw en los dos lados. Como sea que lo consigas, llega en una sola línea larga, que es para lo que está esta página.