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

このガイドの目的は、Node.jsにおけるHTTPハンドリングのプロセスについての確かな理解を伝えることです。言語やプログラミング環境に関わらず、HTTPリクエストがどのように機能するかを一般的に理解していることを前提とします。また、Node.jsのEventEmittersStreamsについて多少の知識があることも前提とします。もしそれらに詳しくない場合は、それぞれのAPIドキュメントをざっと読んでおく価値があります。

サーバの作成

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

const  = ('node:http');

const  = .((, ) => {
  // magic happens here!
});

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

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

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

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

メソッド、URL、ヘッダ

リクエストを処理する際、おそらく最初にやりたいことは、メソッドとURLを調べて適切なアクションを取ることでしょう。Node.jsは、requestオブジェクトに便利なプロパティを配置することで、これを比較的簡単にしてくれます。

const { ,  } = request;

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

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

ヘッダもすぐ近くにあります。それらはrequestheadersという名前の独自のオブジェクト内にあります。

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

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

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

リクエストボディ

POSTPUTリクエストを受け取る際、リクエストボディはアプリケーションにとって重要かもしれません。ボディデータを取得するのは、リクエストヘッダにアクセスするよりも少し手間がかかります。ハンドラに渡されるrequestオブジェクトは、ReadableStreamインターフェースを実装しています。このストリームは、他のどのストリームとも同じように、リッスンしたり、他の場所にパイプしたりできます。ストリームの'data'イベントと'end'イベントをリッスンすることで、ストリームから直接データを取得できます。

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

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

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

エラーに関する簡単な注意点

requestオブジェクトはReadableStreamなので、EventEmitterでもあり、エラーが発生したときにはそのように振る舞います。

requestストリームのエラーは、ストリーム上で'error'イベントを発行することで現れます。そのイベントのリスナーがない場合、エラーはスローされ、Node.jsプログラムがクラッシュする可能性があります。そのため、たとえログを記録して処理を続けるだけであっても、リクエストストリームに'error'リスナーを追加するべきです。(ただし、何らかのHTTPエラーレスポンスを送信するのが最善でしょう。これについては後述します。)

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

これらのエラーを処理する他の方法もありますが、他の抽象化やツールなど、エラーは発生しうるものであり、対処する必要があることを常に意識してください。

ここまでのまとめ

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

const  = ('node:http');


  .((, ) => {
    const { , ,  } = ;
    let  = [];
    
      .('error',  => {
        .();
      })
      .('data',  => {
        .();
      })
      .('end', () => {
         = .().();
        // 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.
      });
  })
  .(8080); // Activates this server, listening on port 8080.

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

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

HTTPステータスコード

特に設定しない場合、レスポンスのHTTPステータスコードは常に200になります。もちろん、すべてのHTTPレスポンスがこれを保証するわけではなく、どこかの時点で異なるステータスコードを送信したくなるでしょう。そのためには、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が適切なタイミングでヘッダを送信してくれることを期待しているということです。

必要であれば、ヘッダをレスポンスストリームに明示的に書き込むことができます。そのためには、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  = ('node:http');


  .((, ) => {
    const { , ,  } = ;
    let  = [];
    
      .('error',  => {
        .();
      })
      .('data',  => {
        .();
      })
      .('end', () => {
         = .().();
        // BEGINNING OF NEW STUFF

        .('error',  => {
          .();
        });

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

        const  = { , , ,  };

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

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

エコーサーバーの例

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

const  = ('node:http');


  .((, ) => {
    let  = [];
    
      .('data',  => {
        .();
      })
      .('end', () => {
         = .().();
        .();
      });
  })
  .(8080);

では、これを少し調整してみましょう。以下の条件の場合にのみエコーを送信するようにします。

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

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

const  = ('node:http');


  .((, ) => {
    if (. === 'POST' && . === '/echo') {
      let  = [];
      
        .('data',  => {
          .();
        })
        .('end', () => {
           = .().();
          .();
        });
    } else {
      . = 404;
      .();
    }
  })
  .(8080);

このようにURLをチェックすることで、「ルーティング」の一種を行っています。他のルーティング形式は、switch文のような単純なものから、expressのようなフレームワーク全体まで様々です。ルーティングだけを行うものを探しているなら、routerを試してみてください。

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

const  = ('node:http');


  .((, ) => {
    if (. === 'POST' && . === '/echo') {
      .();
    } else {
      . = 404;
      .();
    }
  })
  .(8080);

ストリーム万歳!

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

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

レスポンスについては、エラーをstderrにログ出力するだけにします。

const  = ('node:http');


  .((, ) => {
    .('error',  => {
      .();
      . = 400;
      .();
    });
    .('error',  => {
      .();
    });
    if (. === 'POST' && . === '/echo') {
      .();
    } else {
      . = 404;
      .();
    }
  })
  .(8080);

これで、HTTPリクエストの処理に関する基本的なことのほとんどをカバーしました。この時点で、以下のことができるはずです。

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

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