HTTPトランザクションの構造

このガイドの目的は、Node.jsのHTTP処理プロセスをしっかりと理解してもらうことです。言語やプログラミング環境に関係なく、HTTPリクエストの仕組みを一般的な意味で理解していることを前提としています。また、Node.jsのEventEmittersStreamsにある程度の精通していることを前提としています。もしこれらにあまり慣れていない場合は、それぞれのAPIドキュメントをざっと読んでおくことをお勧めします。

サーバーの作成

あらゆるNode Webサーバーアプリケーションは、いずれかの時点でWebサーバーオブジェクトを作成する必要があります。これはcreateServerを使用して行います。

const http = require('node:http');

const server = http.createServer((request, response) => {
  // magic happens here!
});

createServerに渡される関数は、そのサーバーに対して行われたすべてのHTTPリクエストに対して一度だけ呼び出されるため、リクエストハンドラと呼ばれます。実際、createServerによって返されるServerオブジェクトはEventEmitterであり、ここで行っているのは、`server`オブジェクトを作成し、後でリスナーを追加するための単なる省略形です。

const server = http.createServer();
server.on('request', (request, response) => {
  // the same kind of magic happens here!
});

HTTPリクエストがサーバーに到達すると、Nodeはトランザクションを処理するための便利なオブジェクトである`request`と`response`を使用して、リクエストハンドラ関数を呼び出します。これらについては後ほど説明します。

実際にリクエストを処理するには、`server`オブジェクトに対して`listen`メソッドを呼び出す必要があります。ほとんどの場合、`listen`に渡す必要があるのは、サーバーがリッスンするポート番号だけです。他にもオプションがあるので、APIリファレンスを参照してください。

メソッド、URL、ヘッダー

リクエストを処理する際、最初に行いたいことは、メソッドとURLを確認して、適切なアクションを実行できるようにすることです。Node.jsは、`request`オブジェクトに便利なプロパティを配置することで、これを比較的簡単に行えます。

const { method, url } = request;

`request`オブジェクトは`IncomingMessage`のインスタンスです。

ここでの`method`は、常に通常のHTTPメソッド/動詞になります。`url`は、サーバー、プロトコル、ポートを含まない完全なURLです。一般的なURLの場合、これは3番目のスラッシュ以降のすべてを意味します。

ヘッダーも遠く離れていません。`request`上の`headers`という独自のオブジェクトにあります。

const { headers } = request;
const userAgent = headers['user-agent'];

ここで重要なのは、クライアントが実際にどのように送信したかに関係なく、すべてのヘッダーは小文字のみで表されるということです。これは、どのような目的であっても、ヘッダーの解析タスクを簡素化します。

一部のヘッダーが繰り返される場合、ヘッダーによっては、それらの値は上書きされるか、カンマ区切り文字列として結合されます。場合によっては、これが問題になることがあるため、`rawHeaders`も利用できます。

リクエストボディ

`POST`または`PUT`リクエストを受信する場合、リクエストボディがアプリケーションにとって重要になる可能性があります。ボディデータを取得するには、リクエストヘッダーにアクセスするよりも少し複雑です。ハンドラに渡される`request`オブジェクトは、`ReadableStream`インターフェースを実装しています。このストリームは、他のストリームと同様に、リッスンしたり、他の場所にパイプしたりできます。ストリームの`'data'`イベントと`'end'`イベントをリッスンすることで、ストリームから直接データを取得できます。

各`'data'`イベントで出力されるチャンクは`Buffer`です。文字列データになることがわかっている場合は、データを配列に収集し、`'end'`で連結して文字列化するのが最善の方法です。

let body = [];
request
  .on('data', chunk => {
    body.push(chunk);
  })
  .on('end', () => {
    body = Buffer.concat(body).toString();
    // at this point, `body` has the entire request body stored in it as a string
  });

これは少し面倒に思えるかもしれませんし、多くの場合、実際面倒です。幸いなことに、`concat-stream``body`のようなモジュールが`npm`上にあり、このロジックの一部を隠すのに役立ちます。その道に進む前に、何が起こっているのかを十分に理解することが重要であり、そのため、あなたはここにいるのです!

エラーに関する簡単なこと

`request`オブジェクトは`ReadableStream`であるため、`EventEmitter`でもあり、エラーが発生したときも同様に動作します。

`request`ストリームのエラーは、ストリームで`'error'`イベントを発生させることで発生します。**このイベントのリスナーがない場合、エラーが*スロー*され、Node.jsプログラムがクラッシュする可能性があります。**そのため、単にログに記録して処理を続ける場合でも、リクエストストリームに`'error'`リスナーを追加する必要があります。(ただし、何らかのHTTPエラーレスポンスを送信するのがおそらく最善です。詳細については後述します。)

request.on('error', err => {
  // This prints the error message and stack trace to `stderr`.
  console.error(err.stack);
});

他の抽象化やツールなど、これらのエラーを処理する方法は他にもありますが、エラーは発生する可能性があり、実際に発生することを常に認識し、対処する必要があります。

これまでのまとめ

この時点では、サーバーの作成、リクエストからのメソッド、URL、ヘッダー、ボディの取得について説明しました。これらをすべてまとめると、次のようになります。

const http = require('node:http');

http
  .createServer((request, response) => {
    const { headers, method, url } = request;
    let body = [];
    request
      .on('error', err => {
        console.error(err);
      })
      .on('data', chunk => {
        body.push(chunk);
      })
      .on('end', () => {
        body = Buffer.concat(body).toString();
        // At this point, we have the headers, method, url and body, and can now
        // do whatever we need to in order to respond to this request.
      });
  })
  .listen(8080); // Activates this server, listening on port 8080.

この例を実行すると、リクエストを*受信*できますが、*応答*できません。実際、Webブラウザでこの例にアクセスすると、クライアントに何も送信されないため、リクエストはタイムアウトになります。

これまでのところ、`response`オブジェクトにはまったく触れていません。これは`ServerResponse`のインスタンスであり、`WritableStream`です。クライアントにデータを送信するための多くの便利なメソッドが含まれています。次に、これについて説明します。

HTTPステータスコード

設定しない場合、レスポンスのHTTPステータスコードは常に200になります。もちろん、すべてのHTTPレスポンスがこれを保証するわけではなく、いずれかの時点で異なるステータスコードを送信する必要があるでしょう。そのためには、`statusCode`プロパティを設定します。

response.statusCode = 404; // Tell the client that the resource wasn't found.

後ほど説明しますが、これには他にもいくつかのショートカットがあります。

レスポンスヘッダーの設定

ヘッダーは、`setHeader`という便利なメソッドを使用して設定されます。

response.setHeader('Content-Type', 'application/json');
response.setHeader('X-Powered-By', 'bacon');

レスポンスにヘッダーを設定する際、ヘッダー名は大文字と小文字を区別しません。同じヘッダーを繰り返し設定した場合、最後に設定した値が送信されます。

ヘッダーデータの明示的な送信

これまでに説明したヘッダーとステータスコードの設定方法は、「暗黙的なヘッダー」を使用することを前提としています。これは、ボディデータの送信を開始する前に、適切なタイミングでNode.jsがヘッダーを送信することを期待していることを意味します。

必要に応じて、レスポンスストリームにヘッダーを*明示的に*書き込むことができます。そのためには、`writeHead`というメソッドがあり、これを使用してステータスコードとヘッダーをストリームに書き込みます。writeHead

response.writeHead(200, {
  'Content-Type': 'application/json',
  'X-Powered-By': 'bacon',
});

ヘッダーを(暗黙的または明示的に)設定したら、レスポンスデータの送信を開始する準備が整います。

レスポンスボディの送信

`response`オブジェクトはWritableStreamであるため、クライアントにレスポンスボディを書き込むには、通常のストリームメソッドを使用するだけです。

response.write('<html>');
response.write('<body>');
response.write('<h1>Hello, World!</h1>');
response.write('</body>');
response.write('</html>');
response.end();

ストリームの`end`関数は、ストリームの最後のデータとして送信するオプションのデータを受け取ることもできるため、上記の例を次のように簡略化できます。

response.end('<html><body><h1>Hello, World!</h1></body></html>');

ボディにデータチャンクの書き込みを開始する*前に*、ステータスとヘッダーを設定することが重要です。HTTPレスポンスではヘッダーがボディの前に来るため、これは理にかなっています。

エラーに関するもう一つの注意点

`response`ストリームは`'error'`イベントも発生させる可能性があり、いずれはそれも処理する必要があります。`request`ストリームエラーに関するすべてのアドバイスは、ここでも適用されます。

すべてをまとめる

HTTPレスポンスの作成について学習したので、すべてをまとめてみましょう。前の例に基づいて、ユーザーから送信されたすべてのデータを返すサーバーを作成します。`JSON.stringify`を使用して、そのデータをJSONとしてフォーマットします。

const http = require('node:http');

http
  .createServer((request, response) => {
    const { headers, method, url } = request;
    let body = [];
    request
      .on('error', err => {
        console.error(err);
      })
      .on('data', chunk => {
        body.push(chunk);
      })
      .on('end', () => {
        body = Buffer.concat(body).toString();
        // BEGINNING OF NEW STUFF

        response.on('error', err => {
          console.error(err);
        });

        response.statusCode = 200;
        response.setHeader('Content-Type', 'application/json');
        // Note: the 2 lines above could be replaced with this next one:
        // response.writeHead(200, {'Content-Type': 'application/json'})

        const responseBody = { headers, method, url, body };

        response.write(JSON.stringify(responseBody));
        response.end();
        // Note: the 2 lines above could be replaced with this next one:
        // response.end(JSON.stringify(responseBody))

        // END OF NEW STUFF
      });
  })
  .listen(8080);

エコーサーバーの例

前の例を簡略化して、単純なエコーサーバーを作成してみましょう。これは、リクエストで受信したデータをそのままレスポンスで返送するだけです。必要なのは、リクエストストリームからデータを取得し、そのデータをレスポンスストリームに書き込むことだけです。これは、前に行ったことと似ています。

const http = require('node:http');

http
  .createServer((request, response) => {
    let body = [];
    request
      .on('data', chunk => {
        body.push(chunk);
      })
      .on('end', () => {
        body = Buffer.concat(body).toString();
        response.end(body);
      });
  })
  .listen(8080);

それでは、これを調整してみましょう。以下の条件下でのみエコーを送信したいとします。

  • リクエストメソッドがPOSTである。
  • URLが`/echo`である。

それ以外の場合は、単に404で応答します。

const http = require('node:http');

http
  .createServer((request, response) => {
    if (request.method === 'POST' && request.url === '/echo') {
      let body = [];
      request
        .on('data', chunk => {
          body.push(chunk);
        })
        .on('end', () => {
          body = Buffer.concat(body).toString();
          response.end(body);
        });
    } else {
      response.statusCode = 404;
      response.end();
    }
  })
  .listen(8080);

このようにURLをチェックすることで、「ルーティング」の一種を実行しています。他の形式のルーティングは、`switch`ステートメントのように単純なものから、expressのようなフレームワーク全体のように複雑なものまであります。ルーティングのみを行うものをお探しの場合は、routerをお試しください。

素晴らしい!それでは、これを簡略化してみましょう。`request`オブジェクトはReadableStreamであり、`response`オブジェクトはWritableStreamであることを思い出してください。つまり、pipeを使用して、一方から他方へデータを転送できます。これはまさにエコーサーバーに必要なものです!

const http = require('node:http');

http
  .createServer((request, response) => {
    if (request.method === 'POST' && request.url === '/echo') {
      request.pipe(response);
    } else {
      response.statusCode = 404;
      response.end();
    }
  })
  .listen(8080);

ストリームは素晴らしい!

しかし、まだ終わりではありません。このガイドで何度も述べたように、エラーは発生する可能性があり、実際に発生します。そして、それらに対処する必要があります。

リクエストストリームのエラーを処理するために、エラーを`stderr`に記録し、`Bad Request`を示す400ステータスコードを送信します。ただし、実際のアプリケーションでは、エラーを調べて正しいステータスコードとメッセージを特定する必要があります。エラーが発生した場合は、通常どおりErrorドキュメントを参照してください。

レスポンスでは、エラーを`stderr`に記録するだけです。

const http = require('node:http');

http
  .createServer((request, response) => {
    request.on('error', err => {
      console.error(err);
      response.statusCode = 400;
      response.end();
    });
    response.on('error', err => {
      console.error(err);
    });
    if (request.method === 'POST' && request.url === '/echo') {
      request.pipe(response);
    } else {
      response.statusCode = 404;
      response.end();
    }
  })
  .listen(8080);

これで、HTTPリクエストを処理するための基本事項のほとんどを説明しました。この時点で、以下のことができるはずです。

  • リクエストハンドラー関数を使用してHTTPサーバーをインスタンス化し、ポートでリッスンさせる。
  • `request`オブジェクトからヘッダー、URL、メソッド、ボディデータを取得する。
  • `request`オブジェクトのURLやその他のデータに基づいてルーティングの決定を行う。
  • `response`オブジェクトを介してヘッダー、HTTPステータスコード、ボディデータを送信する。
  • `request`オブジェクトから`response`オブジェクトにデータをパイプする。
  • `request`ストリームと`response`ストリームの両方でストリームエラーを処理する。

これらの基本から、多くの一般的なユースケースに対応するNode.js HTTPサーバーを構築できます。これらのAPIは他にも多くの機能を提供しているため、EventEmitterStreamHTTPのAPIドキュメントを必ず読んでください。