nodejssoap-errorstroubleshooting

Node.js node-soap のよくあるエラー: クライアント未定義、TypeError、名前空間の問題

SOAPless Team9 min read

node-soap は Node.js で SOAP サービスを呼び出す際の定番ライブラリです。長い歴史があり多くの WSDL ベースのサービスに対応していますが、エラーメッセージが不親切で、一部の障害はサイレントに発生します。この記事では、node-soap でよく遭遇するエラーの原因と具体的な修正方法をコード付きで解説します。

エラー 1: TypeError: client.MyOperation is not a function

node-soap で最も多いエラーです。クライアントを作成してオペレーションを呼び出そうとすると、メソッドが存在しないという TypeError が発生します。

const soap = require('soap');

soap.createClient('https://example.com/service?wsdl', (err, client) => {
  // ここでクラッシュする
  client.MyOperation({ id: 1 }, (err, result) => {
    console.log(result);
  });
});
TypeError: client.MyOperation is not a function

発生する理由:

  1. オペレーション名の大文字小文字が異なる: WSDL のオペレーション名は大文字小文字を区別します。GetUsergetUser は別物です。
  2. 特定のサービス/ポート配下にある: 複数のサービスやポートを定義する WSDL では、オペレーションが特定のパス配下にしか存在しません。
  3. WSDL のパースが不完全: クライアントは作成されたものの、WSDL が部分的にしか解析されていない。

修正: 利用可能なオペレーションを確認する

soap.createClient('https://example.com/service?wsdl', (err, client) => {
  if (err) {
    console.error('クライアント作成失敗:', err.message);
    return;
  }

  // API 構造をすべて表示
  const description = client.describe();
  console.log(JSON.stringify(description, null, 2));

  // 出力例:
  // {
  //   "MyService": {
  //     "MyPort": {
  //       "GetUser": { "input": { ... }, "output": { ... } }
  //     }
  //   }
  // }

  // フルパスで呼び出す
  client.MyService.MyPort.GetUser({ id: 1 }, (err, result) => {
    console.log(result);
  });
});

最初に必ず client.describe() を実行してください。node-soap が WSDL から解析したサービス名、ポート名、オペレーション名を正確に確認できます。

エラー 2: コールバックの err が null なのに result が空

サイレントな障害パターンです。コールバックはエラーなしで呼ばれますが、結果が空のオブジェクトか予期しない構造になっています。

client.GetUser({ UserId: 1 }, (err, result) => {
  console.log('Error:', err);   // null
  console.log('Result:', result); // {} または { return: undefined }
});

原因:

  • 名前空間の不一致: リクエスト要素が誤った名前空間に属しているため、サーバーが無視して空のレスポンスを返す。
  • パラメータ名が異なる: WSDL は userId を期待しているが、UserId を送信している。
  • SOAPAction ヘッダーが不正: サーバーが SOAPAction でルーティングしているが、node-soap が誤った値を送信している。

修正: 生の XML を確認する

client.GetUser({ UserId: 1 }, (err, result, rawResponse, soapHeader, rawRequest) => {
  // node-soap が実際に送信した XML
  console.log('送信 XML:\n', rawRequest);
  // サーバーが返した XML
  console.log('受信 XML:\n', rawResponse);
});

修正: SOAPAction を上書きする

// node-soap が使用している SOAPAction を確認
client.GetUser({ UserId: 1 }, (err, result) => {
  console.log('リクエストヘッダー:', client.lastRequestHeaders);
});

// 必要に応じて上書き
client.addHttpHeader('SOAPAction', 'http://example.com/IUserService/GetUser');

エラー 3: WSDL の読み込み失敗

Error: Invalid WSDL URL: https://example.com/service?wsdl
Code: ECONNREFUSED

または以下のようなメッセージが出ることもあります。

Error: Parse Error

修正: 認証とカスタム HTTP オプションを設定する

const soap = require('soap');

const options = {
  wsdl_headers: {
    'Authorization': 'Basic ' + Buffer.from('user:pass').toString('base64'),
  },
  wsdl_options: {
    timeout: 30000,
    // 自己署名証明書用 (開発環境のみ)
    rejectUnauthorized: false,
  },
};

soap.createClient('https://example.com/service?wsdl', options, (err, client) => {
  if (err) {
    console.error('WSDL 読み込み失敗:', err.message);
    return;
  }
  console.log('クライアント作成成功');
});

修正: ローカルファイルから WSDL を読み込む

// 事前にダウンロード: curl -o service.wsdl "https://example.com/service?wsdl"
soap.createClient('./service.wsdl', (err, client) => {
  // ローカル WSDL にはエンドポイント URL がないため手動で設定
  client.setEndpoint('https://example.com/service');
});

エラー 4: async/await パターンの問題

node-soap はコールバックベースで設計されており、Promise ベースの API にはいくつかの罠があります。

誤り: エラーハンドリングなしの createClientAsync

// ハンドルされない Promise rejection が発生する可能性
const client = await soap.createClientAsync('https://example.com/service?wsdl');
const result = await client.GetUserAsync({ id: 1 });
// result は配列: [output, rawResponse, soapHeader, rawRequest]

正しい: 適切な async/await と分割代入

const soap = require('soap');

async function callSoapService() {
  try {
    const client = await soap.createClientAsync('https://example.com/service?wsdl');

    // Async サフィックス付きメソッドは配列を返す
    const [result, rawResponse, soapHeader, rawRequest] =
      await client.GetUserAsync({ id: 1 });

    console.log('Result:', result);
    return result;
  } catch (error) {
    if (error.root && error.root.Envelope) {
      // SOAP Fault の場合
      const fault = error.root.Envelope.Body.Fault;
      console.error('SOAP Fault:', fault.faultstring);
      console.error('Fault detail:', JSON.stringify(fault.detail));
    } else {
      // トランスポート/ネットワークエラー
      console.error('Transport error:', error.message);
    }
    throw error;
  }
}

async API のポイントをまとめます。

  • createClientAsync() はクライアントに解決される Promise を返します。
  • Async サフィックス付きメソッド (例: GetUserAsync) は [result, rawResponse, soapHeader, rawRequest] の配列を返します。
  • SOAP Fault は root.Envelope プロパティを持つエラーとしてスローされます。

エラー 5: SSL/TLS の設定

Error: self-signed certificate in certificate chain

または以下のエラーが出ます。

Error: unable to verify the first certificate

修正: SSL を適切に設定する

const fs = require('fs');

const options = {
  wsdl_options: {
    // カスタム CA 証明書
    ca: fs.readFileSync('/path/to/ca-cert.pem'),
    // mTLS の場合
    cert: fs.readFileSync('/path/to/client-cert.pem'),
    key: fs.readFileSync('/path/to/client-key.pem'),
  },
};

soap.createClient('https://example.com/service?wsdl', options, (err, client) => {
  // WSDL 取得と SOAP 呼び出しの両方で SSL が設定される
});

エラー 6: 名前空間とプレフィックスの問題

node-soap が誤った名前空間プレフィックスを生成し、サーバーがリクエストを拒否したり空の結果を返したりすることがあります。

修正: 名前空間を上書きする

soap.createClient('https://example.com/service?wsdl', (err, client) => {
  // node-soap が見逃した名前空間を追加
  client.addSoapHeader({
    'wsse:Security': {
      attributes: {
        'xmlns:wsse': 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd',
      },
    },
  });

  // 特定の要素の名前空間を上書き
  client.GetUser({
    attributes: {
      'xmlns:tns': 'http://example.com/schemas',
    },
    'tns:UserId': 1,
  }, (err, result) => {
    console.log(result);
  });
});

エラーハンドリングのベストプラクティス

すべての一般的な障害パターンに対応する堅牢なパターンを紹介します。

const soap = require('soap');

class SoapServiceClient {
  constructor(wsdlUrl, options = {}) {
    this.wsdlUrl = wsdlUrl;
    this.options = options;
    this.client = null;
  }

  async connect() {
    this.client = await soap.createClientAsync(this.wsdlUrl, this.options);
    return this;
  }

  async call(operation, params) {
    if (!this.client) {
      throw new Error('Client not connected. Call connect() first.');
    }

    const asyncMethod = this.client[`${operation}Async`];
    if (!asyncMethod) {
      const available = Object.keys(this.client.describe())
        .flatMap(svc => Object.keys(this.client.describe()[svc])
          .flatMap(port => Object.keys(this.client.describe()[svc][port])));
      throw new Error(
        `Operation "${operation}" not found. Available: ${available.join(', ')}`
      );
    }

    try {
      const [result] = await asyncMethod.call(this.client, params);
      return result;
    } catch (error) {
      if (error.root?.Envelope?.Body?.Fault) {
        const fault = error.root.Envelope.Body.Fault;
        throw new Error(`SOAP Fault [${fault.faultcode}]: ${fault.faultstring}`);
      }
      throw error;
    }
  }
}

// 使い方
const svc = new SoapServiceClient('https://example.com/service?wsdl');
await svc.connect();
const user = await svc.call('GetUser', { UserId: 1 });

SOAPless による解決

この記事で取り上げたすべてのエラーは、同じ根本原因から生じています。Node.js アプリケーションが WSDL の理解、XML の構築、名前空間の管理、SOAP 固有のエラーフォーマットの処理を担わなければならないという点です。単純な API 呼び出しに対して、あまりにも多くの複雑さが伴います。

SOAPless はこのレイヤーをすべて排除します。WSDL URL を登録するだけで REST JSON エンドポイントが生成され、fetch() や任意の HTTP クライアントで呼び出せます。node-soap への依存も、名前空間のデバッグも、コールバックと Promise の混乱もありません。リクエストとレスポンスはクリーンな JSON フォーマットで、エラーは標準的な HTTP ステータスコードと JSON ボディで返されます。Node.js アプリケーションが期待するそのままの形式です。

SOAPless は SOAP サービスの OpenAPI 仕様も自動生成するため、Swagger UI や Postman でエンドポイントを探索・テストしてからコードを書き始められます。