Node.js ソケットタイムアウト完全ガイド:原因、設定、トラブルシューティング

2025-05-01

主なポイント

  • 適用範囲
    net モジュール(TCPソケットなど)や tls モジュール(TLS/SSLソケット)など、ネットワーク接続を扱う多くのソケットオブジェクトで設定可能です。HTTPリクエストのタイムアウトとは異なる概念であることに注意してください。
  • 影響
    タイムアウトが発生すると、ソケットは使用できなくなり、それ以降のデータの送受信は失敗します。通常は、ソケットを明示的に終了 (socket.end() または socket.destroy()) する必要があります。
  • イベント
    タイムアウトが発生すると、ソケットオブジェクトから 'timeout' イベントが発行されます。このイベントをリッスンすることで、タイムアウト発生時の処理(例えば、エラーログの記録、接続の再試行、ユーザーへの通知など)を実装できます。
  • 設定
    タイムアウト時間はミリ秒単位で設定します。例えば、socket.setTimeout(5000) と記述すると、5秒間データの送受信がない場合にタイムアウトが発生します。
  • 目的
    無反応な接続を検出し、リソースを無駄に消費するのを防ぐために使用されます。ネットワークの問題、サーバーの過負荷、またはクライアント側の問題など、さまざまな理由で接続が応答しなくなることがあります。

簡単なコード例

const net = require('net');

const client = net.createConnection({ host: 'example.com', port: 80 }, () => {
  console.log('Connected to server!');
  client.setTimeout(3000); // 3秒のタイムアウトを設定

  client.on('timeout', () => {
    console.log('Connection timed out!');
    client.end();
  });

  client.on('data', (data) => {
    console.log('Received data:', data.toString());
    client.end();
  });

  client.on('end', () => {
    console.log('Disconnected from server.');
  });

  client.write('GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n');
});

client.on('error', (err) => {
  console.error('Error:', err);
});

この例では、client.setTimeout(3000) によってソケットに3秒のタイムアウトを設定しています。もしサーバーからの応答が3秒以内にない場合、'timeout' イベントが発生し、コンソールにメッセージが表示された後、接続が終了します。



一般的なエラー

    • 説明
      これは、ソケットが予期せず閉じられた場合に発生する一般的なエラーです。タイムアウトが原因でソケットが閉じられた場合にもこのエラーが発生することがあります。
    • 原因
      • サーバー側がタイムアウト時間内に応答しなかった。
      • ネットワークの遅延や中断により、データが時間内に到達しなかった。
      • サーバー側でエラーが発生し、接続を強制的に閉じた。
      • クライアント側で意図的にソケットを閉じた。
    • トラブルシューティング
      • サーバー側のログを確認し、エラーや遅延がないか調査します。
      • ネットワークの接続状況を確認します(pingコマンドなどで疎通確認)。
      • クライアント側のタイムアウト設定が適切かどうか見直します。
      • サーバー側の処理負荷が高くないか確認します。
  1. Error: read ECONNRESET

    • 説明
      これは、接続が相手側によってリセットされた場合に発生するエラーです。タイムアウト後に相手側が接続を閉じることで発生することがあります。
    • 原因
      • サーバー側がタイムアウトにより接続を終了させた。
      • ネットワークの問題で接続が中断された。
      • サーバー側の設定でアイドルタイムアウトが設定されており、それを超過した。
    • トラブルシューティング
      • サーバー側のタイムアウト設定を確認します。
      • ネットワークの安定性を確認します。
      • クライアント側のタイムアウト処理が適切に行われているか確認します。
  2. 'timeout' イベントが予期せず発生する

    • 説明
      設定したタイムアウト時間よりも早く 'timeout' イベントが発生する場合があります。
    • 原因
      • タイムアウト値の設定が短すぎる。
      • ネットワークの遅延が大きい環境でアプリケーションを実行している。
      • サーバー側の処理が遅く、タイムアウト時間内に応答できない。
    • トラブルシューティング
      • タイムアウトの値をより適切な時間に調整します。
      • ネットワーク環境を確認し、必要であれば改善を検討します。
      • サーバー側のパフォーマンスを調査し、ボトルネックを解消します。
  3. 'timeout' イベントが発生しない

    • 説明
      設定したタイムアウト時間を過ぎても 'timeout' イベントが発生しない場合があります。
    • 原因
      • socket.setTimeout() が正しく呼び出されていない。
      • ソケットがタイムアウト前に閉じられている。
      • イベントリスナーが正しく設定されていない。
    • トラブルシューティング
      • socket.setTimeout() の呼び出し箇所と引数を確認します。
      • ソケットが意図せず閉じられていないか確認します。
      • socket.on('timeout', ...) のイベントリスナーが正しく設定されているか確認します。

一般的なトラブルシューティング

  • コードのレビュー
    クライアント側とサーバー側のコードを見直し、ネットワーク処理に関するロジックに誤りがないか確認します。
  • サーバー側の監視
    サーバーのCPU使用率、メモリ使用量、ネットワーク負荷などを監視し、リソース不足が原因で応答が遅れていないか確認します。
  • エラーハンドリングの実装
    'timeout' イベントや 'error' イベントを適切に処理し、アプリケーションが予期しない終了をしないように対策します。必要に応じて、接続の再試行やユーザーへのエラー通知などを実装します。
  • タイムアウト値の調整
    ネットワーク環境やサーバーの処理能力に合わせて、適切なタイムアウト値を設定します。短すぎると頻繁にタイムアウトが発生し、長すぎると無応答な接続を長時間放置することになります。
  • ネットワークの診断
    pingtraceroutenetstat などのネットワーク診断ツールを使用して、ネットワークの接続状況や遅延、パケットロスなどを調査します。
  • ログの確認
    クライアント側とサーバー側の両方のログを詳細に確認し、エラーメッセージや警告、処理時間などを分析します。
  • WebSocketなどの他のプロトコルを使用している場合も、それぞれのプロトコルやライブラリが提供するタイムアウト設定を確認する必要があります。
  • HTTPリクエストの場合、Node.jsの httphttps モジュールには、ソケットのタイムアウトとは別に、リクエスト全体のタイムアウトを設定するオプション(例えば timeout オプション)が存在します。これらも混同しないように注意が必要です。


例1: クライアント側のタイムアウト設定と処理

この例では、TCPクライアントを作成し、サーバーへの接続時にタイムアウトを設定します。タイムアウトが発生した場合の処理も実装しています。

const net = require('net');

const client = net.createConnection({ host: 'localhost', port: 8080 }, () => {
  console.log('サーバーに接続しました!');
  client.setTimeout(5000); // 5秒のタイムアウトを設定

  client.on('timeout', () => {
    console.log('接続がタイムアウトしました。');
    client.end(); // ソケットを閉じます
  });

  client.on('data', (data) => {
    console.log('サーバーからのデータ:', data.toString());
    client.end();
  });

  client.on('end', () => {
    console.log('サーバーとの接続を閉じました。');
  });

  client.on('error', (err) => {
    console.error('エラー:', err);
  });

  client.write('Hello from client!\r\n');
});

解説

  • client.write(...) でサーバーにデータを送信します。
  • client.on('error', ...) でエラーが発生した場合の処理を記述しています。
  • client.on('end', ...) でサーバーとの接続が閉じられた際の処理を記述しています。
  • client.on('data', ...) でサーバーからデータを受信した際の処理を記述しています。
  • client.on('timeout', ...)'timeout' イベントのリスナーを設定しています。タイムアウトが発生した場合、コンソールにメッセージを表示し、client.end() でソケットを閉じます。
  • client.setTimeout(5000) で、このソケットに5秒のタイムアウトを設定しています。これは、接続が確立されてから、または最後にデータが送受信されてから5秒間何も通信がない場合に 'timeout' イベントが発生することを意味します。
  • net.createConnection() でサーバーへの接続を確立します。

例2: サーバー側のタイムアウト設定と処理

この例では、TCPサーバーを作成し、個々の接続に対してタイムアウトを設定します。

const net = require('net');

const server = net.createServer((socket) => {
  console.log('クライアントが接続しました:', socket.remoteAddress + ':' + socket.remotePort);

  socket.setTimeout(10000); // この接続に対して10秒のタイムアウトを設定

  socket.on('timeout', () => {
    console.log('クライアント (' + socket.remoteAddress + ':' + socket.remotePort + ') がタイムアウトしました。');
    socket.end(); // ソケットを閉じます
  });

  socket.on('data', (data) => {
    console.log('受信データ:', data.toString());
    socket.write('Data received!\r\n');
  });

  socket.on('end', () => {
    console.log('クライアント (' + socket.remoteAddress + ':' + socket.remotePort + ') との接続を閉じました。');
  });

  socket.on('error', (err) => {
    console.error('クライアント (' + socket.remoteAddress + ':' + socket.remotePort + ') でエラー:', err);
  });
});

server.listen(8080, () => {
  console.log('サーバーがポート 8080 で起動しました。');
});

解説

  • server.listen() でサーバーを指定されたポートでリッスンを開始します。
  • 他のイベント ('data', 'end', 'error') の処理はクライアント側と同様です。
  • socket.on('timeout', ...) で、この接続でタイムアウトが発生した場合の処理を記述しています。
  • socket.setTimeout(10000) で、この特定のクライアントとの接続に対して10秒のタイムアウトを設定しています。
  • コールバック関数内の socket オブジェクトは、個々のクライアントとの接続を表します。
  • net.createServer() でTCPサーバーを作成し、新しい接続があるたびにコールバック関数が実行されます。

例3: タイムアウト時間を0に設定してタイムアウトを無効化する

タイムアウトを無効化したい場合は、socket.setTimeout(0) を使用します。

const net = require('net');

const client = net.createConnection({ host: 'localhost', port: 8080 }, () => {
  console.log('サーバーに接続しました!');
  client.setTimeout(0); // タイムアウトを無効化

  client.on('data', (data) => {
    console.log('サーバーからのデータ:', data.toString());
    client.end();
  });

  client.on('end', () => {
    console.log('サーバーとの接続を閉じました。');
  });

  client.on('error', (err) => {
    console.error('エラー:', err);
  });

  client.write('Hello from client!\r\n');
});

解説

  • client.setTimeout(0) を呼び出すことで、このソケットのタイムアウト機能が無効になります。つまり、どれだけデータ送受信がなくても 'timeout' イベントは発生しません。

例4: HTTPクライアントでのタイムアウト設定 (request オプションを使用)

HTTPリクエストを行う場合、http.requesthttps.request のオプションでタイムアウトを設定できます。これはソケットのタイムアウトとは少し異なりますが、関連する概念です。

const http = require('http');

const options = {
  hostname: 'example.com',
  port: 80,
  path: '/',
  method: 'GET',
  timeout: 3000 // リクエスト全体のタイムアウトを3秒に設定
};

const req = http.request(options, (res) => {
  console.log(`ステータスコード: ${res.statusCode}`);
  res.setEncoding('utf8');
  res.on('data', (chunk) => {
    console.log(`ボディ: ${chunk}`);
  });
  res.on('end', () => {
    console.log('レスポンスの受信が完了しました。');
  });
});

req.on('error', (err) => {
  console.error(`リクエストエラー: ${err.message}`);
});

req.on('timeout', () => {
  console.log('HTTPリクエストがタイムアウトしました。');
  req.abort(); // リクエストを中止します
});

req.end();
  • タイムアウト時には req.abort() を呼び出してリクエストを中止することが推奨されます。
  • タイムアウトが発生すると、'timeout' イベントが req オブジェクトから発行されます。
  • http.requestoptions オブジェクトに timeout プロパティを設定することで、リクエスト全体のタイムアウト時間をミリ秒単位で指定できます。


http および https モジュールのタイムアウトオプション

HTTPまたはHTTPS通信を行う場合、net.Socket オブジェクトを直接操作する代わりに、httphttps モジュールが提供する高レベルなAPIを使用することが一般的です。これらのモジュールには、リクエスト全体のタイムアウトを設定するオプションが用意されています。

const http = require('http');

const options = {
  hostname: 'example.com',
  port: 80,
  path: '/',
  method: 'GET',
  timeout: 5000 // リクエスト全体のタイムアウトを5秒に設定
};

const req = http.request(options, (res) => {
  // レスポンス処理
});

req.on('error', (err) => {
  console.error('リクエストエラー:', err);
});

req.on('timeout', () => {
  console.log('HTTPリクエストがタイムアウトしました。');
  req.abort(); // リクエストを中止
});

req.end();

解説

  • タイムアウト時の処理として、req.abort() を呼び出してリクエストを強制的に終了させることが推奨されます。
  • タイムアウトが発生すると、'timeout' イベントが req オブジェクトから発行されます。
  • http.request()options オブジェクトに timeout プロパティを設定することで、指定したミリ秒後にリクエストがタイムアウトします。

fetch API (Node.js >= 18)

比較的新しいNode.jsのバージョンでは、ブラウザでお馴染みの fetch API を使用してHTTPリクエストを行うことができます。fetch API にもタイムアウトを設定するオプションがあります。

async function fetchData() {
  try {
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒後にタイムアウト

    const response = await fetch('http://example.com/', { signal: controller.signal });

    clearTimeout(timeoutId);

    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }

    const data = await response.text();
    console.log('Data:', data);
  } catch (error) {
    if (error.name === 'AbortError') {
      console.log('Fetch request timed out');
    } else {
      console.error('Fetch error:', error);
    }
  }
}

fetchData();

解説

  • エラーハンドリングで error.name === 'AbortError' をチェックすることで、タイムアウトによるエラーを特定できます。
  • fetch 関数のオプションに signal: controller.signal を渡すことで、タイムアウト時にリクエストが中断されます。
  • setTimeout を使用して指定時間後に controller.abort() を呼び出し、フェッチリクエストを中止します。
  • AbortController を使用して、リクエストを中止するためのシグナルを作成します。

サードパーティのHTTPクライアントライブラリ

axiosnode-fetch (Node.jsの古いバージョン向け) などのサードパーティのHTTPクライアントライブラリは、より柔軟なタイムアウト設定やリトライ機能などを提供している場合があります。

例 (axios)

const axios = require('axios');

async function fetchDataWithTimeout() {
  try {
    const response = await axios.get('http://example.com/', {
      timeout: 5000 // リクエスト全体のタイムアウトを5秒に設定
    });
    console.log('Data:', response.data);
  } catch (error) {
    if (error.code === 'ECONNABORTED' && error.message.includes('timeout')) {
      console.log('Axios request timed out');
    } else {
      console.error('Axios error:', error);
    }
  }
}

fetchDataWithTimeout();

解説

  • タイムアウトが発生した場合、通常は ECONNABORTED というエラーコードと "timeout of X ms exceeded" というメッセージを含むエラーがスローされます。
  • axios.get() のオプションに timeout プロパティを設定することで、リクエストのタイムアウト時間を指定できます。

ラッパー関数の作成

net.Socket を直接使用する場合でも、タイムアウト処理をより抽象化するために、タイムアウト処理を組み込んだラッパー関数を作成することができます。

const net = require('net');

function connectWithTimeout(options, timeoutMs) {
  return new Promise((resolve, reject) => {
    const socket = net.createConnection(options, () => {
      socket.setTimeout(timeoutMs);
      resolve(socket);
    });

    let timeoutId = setTimeout(() => {
      socket.destroy(new Error('Connection timed out'));
    }, timeoutMs);

    socket.on('data', (data) => {
      clearTimeout(timeoutId); // データ受信でタイムアウトをクリア
    });

    socket.on('end', () => {
      clearTimeout(timeoutId);
    });

    socket.on('error', (err) => {
      clearTimeout(timeoutId);
      reject(err);
    });

    socket.on('timeout', () => {
      socket.destroy(new Error('Socket timed out due to inactivity'));
    });
  });
}

async function main() {
  try {
    const socket = await connectWithTimeout({ host: 'localhost', port: 8080 }, 3000);
    console.log('Connected!');
    socket.write('Hello\r\n');
    socket.on('data', (data) => console.log('Received:', data.toString()));
    socket.on('end', () => console.log('Disconnected'));
  } catch (error) {
    console.error('Error:', error.message);
  }
}

main();
  • setTimeout を使用して接続全体のタイムアウトを管理し、ソケットの 'timeout' イベントも処理しています。
  • connectWithTimeout 関数は、指定されたオプションでソケット接続を試み、指定されたタイムアウト時間内に接続またはデータ送受信がない場合にエラーで reject される Promise を返します。