.NET 8のBasicHttpsBinding+OperationContextScopeでSOAPがServer Errorになる原因と直し方(WCFサービス参照/HttpClient)

Postmanでは通るのに、.NET 8(.NET Core 8.0)のOperationContextScope+BasicHttpsBindingでSOAPを呼ぶと「Server Error(500)」になる——この手の差分は、SOAP 1.1/1.2やSOAPAction、HTTPヘッダーの“わずかなズレ”が原因で起きがちです。本記事では原因の切り分け方と、確実に直す2つの実装(WCFサービス参照/HttpClient)を具体例つきで解説します。

目次

まず結論:最短で直すなら「サービス参照」か「HttpClient」に寄せる

外部のSOAPサービス(例:https://service.leads360.com/ClientService.asmx)に対して、文字列でSOAP XMLを組み立て、ChannelFactory<ILeadsService>+OperationContextScopeでHTTPヘッダーを操作して送る方法は、動く環境では動きますが壊れやすいのが最大の難点です。Postmanは“生のHTTP”をそのまま送るため成功していても、WCF側が生成するメッセージ(SOAPのバージョンやヘッダー)と手書きのXML/HTTPヘッダーが噛み合わないと、サーバーは容赦なく500を返します。

方法向いているケースメリット注意点
WCF「サービス参照の追加」(WSDLからプロキシ生成)長期運用/型安全に呼びたい/メソッドが多いSOAPの面倒(SOAPAction、名前空間、MessageVersionなど)を自動で一致させやすいWSDL更新時に再生成が必要。生成コードの差分管理が必要になることも
HttpClientで“Postmanと同じHTTP”を送るまず動かしたい/対象がレガシー.asmxで癖が強い/1〜数メソッドだけPostmanの成功条件をそのまま再現できる。差分比較が簡単XML組み立て・エスケープ・エラー処理などは自前。テストを厚めに

なぜPostmanは成功し、.NET 8のWCF実装は失敗しやすいのか

結論から言うと、SOAPは「XMLさえ送ればOK」ではなく、HTTPヘッダー・SOAPバージョン・Action・名前空間がセットで一致して初めて同じリクエストになります。Postmanと.NET側で、見た目は似ていても以下の差があると別物になります。

最大の落とし穴:SOAP 1.1とSOAP 1.2(Content-TypeとSOAPAction)の取り違え

質問のコードでは、Content-Type: application/soap+xml; charset=utf-8を指定していました。これはSOAP 1.2で一般的な指定です。一方、古い.asmx系のサービスは、今でもSOAP 1.1(text/xml)+SOAPActionヘッダー必須の実装が多く、ここがズレるとサーバーが500を返す典型パターンになります。

項目SOAP 1.1(.asmxでよくある)SOAP 1.2
Content-Typetext/xml; charset=utf-8application/soap+xml; charset=utf-8
Actionの指定HTTPヘッダーにSOAPAction: "..."が入ることが多いContent-Typeのパラメータにaction="..."を付けることが多い
Envelope名前空間http://schemas.xmlsoap.org/soap/envelope/http://www.w3.org/2003/05/soap-envelope
サーバー側の実装レガシー実装が多く、厳密にヘッダー一致を要求することがある比較的新しい実装が多いが、SOAP 1.1を受けない場合もある

つまり、PostmanがSOAP 1.1相当(text/xml+SOAPAction)で送って成功しているなら、.NET側も同じバージョン・同じヘッダーで送らないと再現できません。application/soap+xmlにした時点で、サーバーがSOAP 1.2として解釈し、期待するEnvelopeやActionが一致せずエラー、という流れが起きやすくなります。

OperationContextScopeでヘッダーを上書きすると「WCFが想定するメッセージ」と齟齬が出やすい

OperationContextScopeは、WCFのメッセージ送受信に付随するコンテキスト(ヘッダーやプロパティ)を操作するための仕組みです。しかし、バインディングが選ぶMessageVersion(SOAP 1.1/1.2)と、手動で上書きするHTTPヘッダーが噛み合わないと、表面的にヘッダーはそれっぽく見えても中身の整合性が崩れます。

例えば、バインディングがSOAP 1.1のメッセージを前提にしているのに、HTTPのContent-TypeだけSOAP 1.2に寄せると、サーバーは「SOAP 1.2が来た」と思ってSOAP 1.2の規則で解析し、Envelopeの名前空間やActionの場所が違っていて処理できず500、ということが起きます。

HTTPレベルの差分でもレガシーSOAPはコケることがある

SOAPサービスが古い場合、XMLの内容だけでなくHTTPの振る舞いの差(特にHTTPS越し)で失敗することがあります。代表例は以下です。

  • Transfer-Encoding: chunked(チャンク送信)を受け付けない
  • Expect: 100-continueのハンドシェイクが苦手
  • HTTP/2のネゴシエーションや中間プロキシとの相性
  • 圧縮(gzip/deflate)やKeep-Aliveの扱い

Postmanは比較的“普通のContent-Length付きPOST”になりやすい一方、ライブラリや設定によってはWCF/HTTPスタックが別の送信形態になることがあります。ここも「PostmanはOKなのに.NETはNG」の差分要因になり得ます。

解決策①:Visual Studioの「サービス参照の追加(WCF)」でクライアントを自動生成する

長く安定して運用するなら、この方法が最も堅いです。WSDLから生成されたプロキシは、サービスが期待するSOAPバージョン、Action、名前空間、シリアライズ形式などを、基本的にサービス定義に合わせて作ってくれます。

手順(Visual Studio)

  • プロジェクトを右クリックし、「追加」→「接続済みサービス」(または「サービス参照の追加」)を選ぶ
  • WCF Webサービス参照を選択し、WSDLのURLを入力する
  • 例:https://service.leads360.com/ClientService.asmx?WSDL
  • 名前空間などを決めて完了すると、プロキシクラスと設定が生成される

生成したクライアントの呼び出し例(イメージ)

生成される型名・メソッド名はサービスごとに異なりますが、呼び出しは概ね次の形になります。なお、BasicHttpsBindingを使う場合も基本は同じで、HTTPS(TLS)で送る設定になっていることが重要です。BasicHttpBinding(BasicHttpSecurityMode.Transport)は「HTTPSでBasicHttp相当を使う」定番の書き方です。

using System.ServiceModel;

var binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport)
{
    MaxReceivedMessageSize = 10 * 1024 * 1024,
    ReaderQuotas = System.Xml.XmlDictionaryReaderQuotas.Max
};

var endpoint = new EndpointAddress("https://service.leads360.com/ClientService.asmx");

// 例:WSDLから生成されたクライアント(型名は環境で変わります)
var client = new ClientServiceSoapClient(binding, endpoint);

// 例:WSDLに定義された操作を型安全に呼び出す
var result = await client.SomeOperationAsync(/* パラメータ */);

Console.WriteLine(result);

このやり方の利点は、自分でSOAP XMLを組み立てなくて済むことです。特に、名前空間の付け方や要素名の大文字小文字、配列・nullの表現など、手書きで揃えると事故りやすいポイントをまとめて回避できます。

CLIで生成したい場合:dotnet-svcutil(VSなしでも可)

CIや非Windows環境、またはVisual Studioを使わない運用では、WSDLからクライアントを生成できるツールを使う方法もあります。代表的なのがdotnet-svcutilです。

# 例:ツールのインストール(環境により手順は異なります)
dotnet tool install --global dotnet-svcutil

# 例:WSDLからプロキシ生成(出力ファイル名などは任意)

dotnet-svcutil [https://service.leads360.com/ClientService.asmx?WSDL](https://service.leads360.com/ClientService.asmx?WSDL) -o Leads360Client.cs

生成物をプロジェクトに取り込み、必要に応じてバインディング(HTTPS、タイムアウト、最大サイズ)を調整します。

サービス参照方式が効く理由

Postmanで成功しているSOAP呼び出しは、WSDLで定義されている「正しい呼び出し規約」を満たしていることが多いです。サービス参照は、その規約をコードとして取り込み、呼び出し側のミス余地を減らすのが本質的な価値です。SOAPに不慣れな場合ほど、まずは生成プロキシをベースに動作確認すると最短で安定します。

解決策②:HttpClientで「Postmanと同じHTTP」を送る(コメントで解決したパターン)

質問者が最終的に解決したのがこのルートです。WCF経由で“SOAPっぽいこと”をするのではなく、Postmanと同じようにHTTP POSTとしてSOAP XMLを送ります。ポイントは「Postmanで成功したリクエストのヘッダーとボディを、可能な限り同一にする」ことです。

まずはPostmanの成功条件を固定する

次の情報は必ず控えておきます(ここが揃えば、原因はほぼ潰せます)。

  • 送信先URL(末尾のスラッシュ有無も含めて完全一致)
  • Content-Type(text/xmlかapplication/soap+xmlか)
  • SOAPAction(存在するか、値が何か、引用符の有無)
  • SOAP Envelopeの名前空間(SOAP 1.1/1.2)
  • Bodyの操作名とその名前空間(xmlns)

HttpClientの実装例(SOAP 1.1:text/xml + SOAPAction)

.asmxで最も相性が良いのは、まずSOAP 1.1の形です。Postmanがこの形で成功しているなら、.NET側も同じ形に寄せます。

using System;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

public class SoapCaller
{
    public static async Task<string> CallAsync()
    {
        var url = "https://service.leads360.com/ClientService.asmx";

        // WSDLに書かれている Action を入れます(例はダミー)
        var soapAction = "http://tempuri.org/YourMethod";

        // Envelope/名前空間/要素名は必ずサービス定義(WSDL)に合わせます
        var soapXml =
@"<?xml version=""1.0"" encoding=""utf-8""?>
<soap:Envelope xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance""
               xmlns:xsd=""http://www.w3.org/2001/XMLSchema""
               xmlns:soap=""http://schemas.xmlsoap.org/soap/envelope/"">
  <soap:Body>
    <YourMethod xmlns=""http://tempuri.org/"">
      <param1>value</param1>
    </YourMethod>
  </soap:Body>
</soap:Envelope>";

        var handler = new HttpClientHandler
        {
            AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
        };

        using var http = new HttpClient(handler);

        using var req = new HttpRequestMessage(HttpMethod.Post, url);

        // SOAP 1.1 の基本:text/xml
        req.Content = new StringContent(soapXml, Encoding.UTF8, "text/xml");

        // SOAPAction(SOAP 1.1 で重要)
        req.Headers.Add("SOAPAction", $"\\"{soapAction}\\"");

        // レガシー対策:HTTP/1.1 固定+Expect: 100-continue無効化
        req.Version = HttpVersion.Version11;
        req.Headers.ExpectContinue = false;

        using var res = await http.SendAsync(req);
        var body = await res.Content.ReadAsStringAsync();

        if (!res.IsSuccessStatusCode)
        {
            throw new HttpRequestException($"HTTP {(int)res.StatusCode} {res.ReasonPhrase}\\n{body}");
        }

        return body;
    }
}

このコードは、WCFのように抽象化しません。その代わり、Postmanと同じHTTPヘッダー・同じXMLを自分の目で一致させながら作れます。サーバーがレガシーであるほど、この“地に足のついた一致”が効きます。

SOAP 1.2で送る場合の例(application/soap+xml + action)

もしサービスがSOAP 1.2を前提にしている(またはWSDLにSOAP 1.2用のbindingがある)場合は、Envelopeの名前空間をSOAP 1.2にし、Content-Typeをapplication/soap+xmlに合わせます。ActionはヘッダーではなくContent-Typeパラメータに入れる実装も多いです。

using var req = new HttpRequestMessage(HttpMethod.Post, url);

req.Content = new StringContent(soapXml, Encoding.UTF8);
req.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(
    $"application/soap+xml; charset=utf-8; action=\\"{soapAction}\\"");

// 一部サービスはSOAPActionヘッダーを要求することもあるため、必要なら併用
// req.Headers.Add("SOAPAction", $"\\"{soapAction}\\"");

ここで重要なのは、Envelopeの名前空間(SOAP 1.1/1.2)とContent-Typeを必ず揃えることです。片方だけを変えると、サーバー側のSOAPパーサーが期待する規則とズレてエラーになりやすくなります。

比較のコツ:Postmanと.NETの“差分”を可視化して潰す

「なぜ同じXMLを送っているのに失敗するのか?」に悩んだら、XMLだけを見るのはやめて、HTTPリクエスト全体を比較します。おすすめは次の流れです。

  • Postmanの送信内容(Headers/Body)をエクスポートする(PostmanのCode生成も使える)
  • .NET側は送信直前のヘッダーとボディをログ出しする
  • 可能ならHTTPSをプロキシでキャプチャし、完全一致に近づける
比較ポイントPostmanで確認すること.NET側で合わせる方法
Content-Typetext/xmlかapplication/soap+xmlかStringContentのmediaTypeを揃える/必要ならContentTypeをParseで指定
SOAPActionヘッダーにあるか、値と引用符req.Headers.Add("SOAPAction", ...)で一致させる
Envelope名前空間SOAP 1.1/1.2のURLXMLをWSDL準拠にする(片方だけ変えない)
URL完全一致(末尾/クエリ含む)同じURIを使う(?WSDLに送っていないか等も確認)
Transfer-EncodingchunkedになっていないかStringContentを使う/HTTP/1.1固定/ストリーミング送信を避ける
Expect: 100-continue付いているかreq.Headers.ExpectContinue = false

それでも500が返るときのチェックリスト(実務で効く順)

サーバーが「Server Error」しか返さない場合、クライアント側で潰すべきポイントを順番に当てていく必要があります。以下は現場で効果が高い順に並べたチェックです。

チェック項目症状対処
SOAP 1.1/1.2の不一致PostmanはOK、.NETは500text/xml+SOAPAction(SOAP 1.1)に寄せる/WSDLのbindingを確認
SOAPActionが違う/無い「Actionが見つからない」系の内部例外で500WSDLのoperation定義から正しいActionを採用。引用符まで一致させる
Bodyの操作名・名前空間が違うメソッドに到達せず500Bodyのルート要素(<YourMethod xmlns=...>)をWSDL準拠に
認証・IP制限環境によって成功/失敗が変わるBasic認証/トークン/許可IPなどを再確認(Postmanと同条件に)
サイズ制限大きいレスポンスで失敗WCFならMaxReceivedMessageSize等、HttpClientなら読み取り制限やタイムアウトを見直す
文字コード/BOM/特殊文字特定データだけ失敗UTF-8(BOMなし)を基本にし、XMLエスケープを徹底する
chunked/100-continue等のHTTP癖古いサーバーでのみ失敗HTTP/1.1固定、ExpectContinue無効、ストリーミング回避

どうしてもWCF+OperationContextScopeで続けたい場合の現実的な落とし所

組織事情でWCF(ChannelFactory)を残す必要がある場合でも、次の方針に寄せると事故が減ります。

  • 手書きXMLをやめる:可能ならWSDLから契約(インターフェース)とデータ型を生成し、型安全に送る
  • バインディングのMessageVersionとContent-Typeを一致させる:SOAP 1.1ならtext/xml、SOAP 1.2ならapplication/soap+xmlに
  • SOAPActionはWSDL準拠:勝手に決めず、定義から拾う

例えばSOAP 1.1を前提にするなら、バインディングをSOAP 1.1として扱い、HTTPヘッダーもその前提で設定します(ヘッダーの上書きは最小限に)。

using System.Net;
using System.ServiceModel;
using System.ServiceModel.Channels;

using (new OperationContextScope((IContextChannel)client))
{
    var http = new HttpRequestMessageProperty();
    http.Headers[HttpRequestHeader.ContentType] = "text/xml; charset=utf-8";
    http.Headers["SOAPAction"] = "\\"http://tempuri.org/YourMethod\\"";

    OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = http;

    // ここでWSDLに沿った操作を呼ぶ(stringで生XMLを投げない)
    var result = await client.YourMethodAsync(...);
}

ただし、ここまでやるなら「そもそもサービス参照で生成した方が早くて安全」という場面が多いのも事実です。手書きXML+OperationContextScopeは最後の手段として考えると良いでしょう。

現場目線のアドバイス:レガシーSOAPを“壊れにくく”運用する

レガシーな.asmx SOAPは、仕様通りに動く一方で「許容範囲が狭い」ことがあります。運用で苦しまないために、次の工夫が効きます。

  • 成功したリクエストを“ゴールデン”として保存:PostmanコレクションやcURL相当をリポジトリに残す
  • リクエスト/レスポンスをログで追えるようにする:500時にボディが返るサービスもある
  • タイムアウトを明示:外部サービスが遅い時の再試行戦略も含めて設計
  • XMLの組み立てはテンプレ+エスケープ:値の埋め込みで壊れないようにする

特にHttpClient方式では、XMLに値を埋め込むときに不正な文字(&、<など)が混ざると簡単に壊れます。テンプレート文字列でやる場合でも、必要に応じてSecurityElement.EscapeやXMLライブラリ(XDocument)で要素を組み立てるなど、エスケープの責務を明確にしておくのが安全です。

まとめ:Postmanで成功しているなら、.NET側は「同じバージョン・同じヘッダー」に揃えれば直る

Postmanで成功しているのに.NET 8のOperationContextScope+WCFで失敗する場合、原因の多くはSOAP 1.1/1.2の不一致、またはSOAPActionや名前空間などの微妙な差分です。確実に直すなら次のどちらかに寄せるのが近道です。

  • WCFサービス参照(WSDL)でプロキシを生成し、型安全に呼ぶ
  • HttpClientでPostmanと同じHTTP(Content-Type/Action/Envelope)を送る

「まず動かす」ならHttpClient、「長期運用」ならサービス参照。この軸で選び、Postmanとの差分を潰していけば、レガシーSOAPでも.NET 8から安定して呼び出せるようになります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次