SOAP リクエストフォーマッター

SOAP エンベロープを整形・検査。認証情報はタブの外に出ません。

入力
出力
待機中文書を貼り付けると検査します。入力中にそのまま検証されます。

すべてこのタブ内で実行されます。貼り付けた内容がアップロード・記録・送信されることはありません。 ネットワークパネルを開いて確認する.

上に SOAP のリクエストかレスポンスを貼り付けると、入力しながら字下げされます。名前空間の接頭辞は薄く表示されるので soap:Body は印のついた Body として読めますし、整形式でない箇所はすべて、行・列・代わりに書くべきものつきで報告されます。属性の制御を 3 にすれば、Envelope は 300 文字の 1 行であることをやめ、xmlns 宣言がそれぞれ自分の行を持ちます。

あなたが貼り付けている封筒は、ほぼ確実にログから出てきたものです。Fiddler のキャプチャ、PHP の SoapClient の __getLastRequest() の出力、WCF のメッセージトレース、SoapUI の raw タブ、あるいは折り返しをしないロガーが午前 3 時に書いた 1 行。その状態では読めませんし、落ち度が自分側か相手側かを言うには、まず読めるようにする必要があります。

何もアップロードしません。そしてここでは、それが売り文句ではなく要点です。SOAP のペイロードは、パスワードの入った WS-Security トークン、署名された SAML アサーション、口座番号、患者記録を運びます。この種の検索で上位に出るバリデーターの 1 つは、あなたのデータが自社サーバーに保存されることに同意するチェックを求めます。SOAP フォーマッターを公開している別のサイトは、送信された文書を公開状態で保存しており、Google がそれを索引しています。ここでは、パーサーもフォーマッターもこのタブの中の JavaScript です。

封筒と、版を見分ける URI

SOAP メッセージは、外側の形が決まった 1 つの XML 文書です。ルートは Envelope。Header を持つことがあり、持つ場合は Header が先に来ます。Body は必ず持ち、その中に操作のペイロードか Fault のどちらかが入ります。Body より下はすべてサービスのものです。

版を見分けるのは名前空間の URI であって、決して接頭辞ではありません。接頭辞は任意で、soap、soapenv、SOAP-ENV、env がいずれも使われています。正しく見えるリクエストにサーバーが VersionMismatch の fault を返したら、ほかの何を見るより先に URI を 1 文字ずつ比べてください。

  • SOAP 1.1:名前空間は http://schemas.xmlsoap.org/soap/envelope/(末尾のスラッシュも一部です)、Content-Type は text/xml、操作は別の SOAPAction ヘッダーに入れ、その値は引用符でくくらなければなりません。空の引用符の対でもかまいません。
  • SOAP 1.2:名前空間は http://www.w3.org/2003/05/soap-envelope、Content-Type は action パラメーターつきの application/soap+xml、SOAPAction ヘッダーはありません。1.2 のエンドポイントに 1.1 の content type を渡すとたいてい HTTP 415 が返り、失敗が通信の問題のように見えてしまいます。
  • SOAP 1.1 は Body の後ろの要素を許していました。SOAP 1.2 は許しません。Envelope の子は Header と Body だけで、Body が最後です。

接頭辞のエラーが SOAP でいちばん多い故障である理由

名前空間の宣言は Envelope 要素にあり、あなたが気にしている部分はその 4 階層下です。ログから面白い断片をコピーすれば、接頭辞だけ持ち出して宣言は置き去りにしたことになります。するとメッセージは原因ではなく接頭辞の名を挙げます。libxml2 は「Namespace prefix soap on Body is not defined」と言い、.NET は「'soap' is an undeclared prefix」と言います。ここの検査は、足すべき宣言とともにそれを報告します。

逆の間違いはもっと静かで、もっとまずいものです。接頭辞なしの断片を、既定名前空間の下にある Body に貼り付けると、その中の要素すべてがその名前空間に移ります。文書は解析され、サービスは受け取り、そしてフィールドは空で返ってきます。貼り付けた断片のルートに xmlns="" を置いて、そこから外れてください。

経験のある人でも引っかかる規則が 1 つ。既定名前空間は要素名に適用され、属性名には決して適用されません。ですから mustUnderstand、actor、role は、封筒の名前空間が既定になっていても封筒の接頭辞を付けなければなりません。

mustUnderstand と、ペイロードが読まれる前に失敗するヘッダー

mustUnderstand が付いたヘッダーブロックは契約です。対象の役割を演じる受信者は、そのブロックを理解するか、さもなければメッセージ全体を MustUnderstand の fault で拒み、ほかを何も処理してはなりません。だから Body が完全に正しいリクエストでも拒まれるのです。サービスは Body にたどり着いてもいません。

値は版によって違い、取り違えはエラーではなく黙った失敗になります。SOAP 1.1 は文字「1」か「0」を定め、既定は「0」です。SOAP 1.2 は xs:boolean として型付けするので「true」「false」も使えます。厳格な 1.1 のスタックに mustUnderstand="true" を送ると、属性は「ない」と読まれ、必須のヘッダーが任意になってしまいます。もう半分は宛先指定です。1.1 は URI つきの actor を使い、1.2 はそれを role に改名して role/none、role/next、role/ultimateReceiver を定め、最後のものが既定です。この層の失敗のほとんどは、その操作にセキュリティポリシーが設定されていないサーバーに対して、mustUnderstand を付けた WS-Security ヘッダーを送ったというものです。

soap:Fault を読む

Fault は Body の中の普通の要素で、ある場合には Body の唯一の子でなければなりません。構造は版の間で完全に変わりました。だから片方の版に合わせて書いた fault の処理が、もう片方では黙って何にも合わなくなるのです。

SOAP 1.1 では Fault の子は修飾されていません。faultcode、faultstring、faultactor、detail は、Fault が封筒の名前空間にあっても名前空間なしに置かれます。ですから //soap:Fault/soap:faultstring は何も返さず、//soap:Fault/faultstring としなければなりません。faultcode は QName を持ち、ふつうは soap:Client(あなたのメッセージが誤っていた)か soap:Server(相手側が失敗した。再試行で通ることもある)です。

SOAP 1.2 はすべてを修飾し、名前も変えました。Code は固定の一覧(Sender、Receiver、VersionMismatch、MustUnderstand、DataEncodingUnknown)から Value を持ち、任意で Subcode を連ねられます。Reason は Text 要素を持ち、それぞれに xml:lang が必要です。残りは Node、Role、Detail が置き換えます。ステータスも情報を運びます。1.1 はどの fault でも 500 を返し、1.2 は Sender に 400、Receiver に 500 を返します。

コードで SOAP を送り、読む

ここに貼り付ける封筒は、ふつうこれらのどれかから出てきたものです。各サンプルはリクエストを送り、成功と決めつける前に Fault を確かめ、レスポンスを安全に解析します。相手方から来た XML であり、Java、PHP、Python の既定は外部実体を解決するからです。

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

6 つすべてに共通する間違いは、HTTP 500 を通信の失敗として扱うことです。SOAP 1.1 の fault はステータス 500 と本文の完全な封筒で届くので、raise_for_status() や EnsureSuccessStatusCode()、素朴な response.ok の確認は、何が起きたのかについて得られる唯一の説明を捨ててしまいます。

よくある質問

私の封筒にはパスワードと顧客記録が入っています。アップロードされますか。

いいえ。パーサーもフォーマッターも、このタブの Web Worker で動く JavaScript です。何かを送る先のエンドポイントもなく、エディターにアクセスできる解析ツールもなく、第三者のスクリプトもありません。

信じるのではなく確かめてください。ネットワークタブを開き、封筒を貼り付けて整形します。ページは自分のアセットを一度読み込み、そのあと静まります。この確認は、このサイトのどこよりここで重要です。WS-Security ヘッダーはパスワードのダイジェストつき UsernameToken を運び、社内サービスでは少なからずパスワードそのものを運ぶからです。入力は、消すまでこのブラウザーの localStorage に留まります。

SOAP 1.1 と SOAP 1.2 の違いは何ですか。

名前空間の URI から始めてください。ほかのすべてはそこから従います。1.1 は http://schemas.xmlsoap.org/soap/envelope/、1.2 は http://www.w3.org/2003/05/soap-envelope です。接頭辞は何も教えてくれません。

通信面では、1.1 は text/xml と別立ての引用符つき SOAPAction ヘッダーを使い、1.2 は action パラメーターつきの application/soap+xml を使って SOAPAction は持ちません。メッセージの中では、1.2 は Fault を書き直してその全部分を修飾し、mustUnderstand を論理型として型付けし、actor を role に改名し、Body より後のアプリケーション要素を禁じました。本番で動いているサービスの多くは今も 1.1 です。

「未宣言の接頭辞」エラーが何度も出るのはなぜですか。

xmlns 宣言は Envelope 要素にあり、あなたはそれより下をコピーしたからです。接頭辞は、それを束縛する宣言が有効範囲にある間だけ意味を持つので、単独で貼り付けた soap:Body は整形式の XML ですらなく、正当な SOAP どころではありません。

貼り付けたものの根に束縛を足してください。1.1 の断片なら xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" です。逆の間違いはエラーをまったく出さないので気をつけてください。既定名前空間の下の Body に接頭辞なしの断片を貼れば、それは黙ってその名前空間に移され、フィールドは空で返ってきます。

これは封筒を SOAP スキーマに照らして検証しますか。

いいえ。そうであるかのように装うのは、誤った親切です。このページは封筒を解析し、整形式でない箇所をすべて行と列つきで報告し、各接頭辞が束縛されているかを確かめ、データに手を触れずに整形し、サイズと行数と要素数を報告します。

SOAP の内容モデルは強制しないので、Header が Body の後にあっても、Body がなくても、1.2 の封筒の中に 1.1 の fault を組んでいても文句は言いません。それらはスキーマの制約です。名前空間 URI で公開されている SOAP 封筒スキーマに照らして、ここの XSD バリデーターで封筒を検証してください。WSDL も読まず、リクエストも送らず、署名の検証もしません。

ここに貼る生の封筒はどこから手に入れますか。

自分のコードからではなくクライアントから取ってください。ライブラリに渡したオブジェクトではなく、実際に線に載ったバイト列が欲しいからです。PHP なら trace を付けて SoapClient を作り、__getLastRequest() を呼びます。Java の SAAJ なら saveChanges() の後に message.writeTo(System.out) を呼びます。.NET なら WCF のメッセージログを有効にします。Python の zeep なら HistoryPlugin を付けて last_sent を読みます。

プロセスの外からなら、curl の --data-binary と -D でレスポンスとヘッダーをファイルに書けますし、Fiddler と mitmproxy は生きた通信を捕まえ、SoapUI は両側に raw タブを持ちます。どう手に入れても、それは長い 1 行で届きます。このページはそのためにあります。

関連ツール

関連する解説

これで解決できるエラー