Skip to content

HTTP トランザクションの解剖

このガイドの目的は、Node.js の HTTP 処理のプロセスに関する確固たる理解を与えることです。言語やプログラミング環境に関係なく、HTTP リクエストの仕組みを大まかに理解していることを前提とします。また、Node.js の EventEmitter と Stream にも多少の familiarity があることを前提とします。あまり慣れていない場合は、それぞれの API ドキュメントを簡単に読んでおく価値があります。

サーバーの作成

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

javascript
const http = require('node:http')
const server = http.createServer((request, response) => {
  // magic happens here!
})

createServerに渡される関数は、そのサーバーに対して行われたすべての HTTP リクエストごとに 1 回呼び出されるため、リクエストハンドラーと呼ばれます。実際、createServerによって返される Server オブジェクトは EventEmitter であり、これはサーバーオブジェクトを作成し、後でリスナーを追加するという簡略表記に過ぎません。

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

HTTP リクエストがサーバーにヒットすると、Node はリクエストハンドラー関数を呼び出し、トランザクション、リクエスト、レスポンスを処理するためのいくつかの便利なオブジェクトを渡します。これらについてはすぐに説明します。リクエストを実際に処理するには、サーバーオブジェクトでlistenメソッドを呼び出す必要があります。ほとんどの場合、listenに渡す必要があるのは、サーバーが listen するポート番号だけです。他のオプションもあるので、API リファレンスを参照してください。

メソッド、URL、ヘッダー

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

javascript
const { method, url } = request

リクエストオブジェクトはIncomingMessageのインスタンスです。ここのメソッドは常に通常の HTTP メソッド/動詞です。url は、サーバー、プロトコル、ポートを除いた完全な URL です。一般的な URL の場合、これは 3 番目のスラッシュ以降を含みます。

ヘッダーもすぐ近くに存在します。それらはリクエスト上のheadersという独自のオブジェクト内にあります。

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

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

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

リクエストボディ

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

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

javascript
let body = []
request.on('data', chunk => {
  body.push(chunk)
})
request.on('end', () => {
  body = Buffer.concat(body).toString()
  // この時点で、'body' にはリクエストボディ全体が文字列として格納されています
})

NOTE

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

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

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

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

javascript
request.on('error', err => {
  // エラーメッセージとスタックトレースを stderr に出力します。
  console.error(err.stack)
})

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

これまでの成果

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

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

http.createServer((request, response) => {
    const { headers, method, url } = request;
    let body = [];
    request.on('error', err => console.error(err));
    request.on('data', chunk => {
        body.push(chunk);
    });
    request.on('end', () => {
        body = Buffer.concat(body).toString();
        // この時点で、ヘッダー、メソッド、URL、ボディを取得しており、このリクエストに応答するために必要な処理を実行できます。
    });
});

.listen(8080); // ポート8080でリスンしてこのサーバーをアクティブにします。

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

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

HTTP ステータスコード

設定しないと、レスポンスの HTTP ステータスコードは常に 200 になります。もちろん、すべての HTTP レスポンスがこれを受けるわけではなく、いつか別のステータスコードを送信したくなるでしょう。そのためには、statusCodeプロパティを設定します。

javascript
response.statusCode = 404 // リソースが見つからないことをクライアントに伝えます。

すぐにわかるように、これにはいくつかのショートカットがあります。

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

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

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

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

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

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

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

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

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

ヘッダーを設定したら(暗黙的か明示的かに関わらず)、レスポンスデータの送信を開始できます。

レスポンスボディの送信

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

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

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

javascript
response.end('<html><body><h1>hello,world!</h1></body></html>')

NOTE

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

エラーに関するもう一つの簡単なこと

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

すべてをまとめる

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

javascript
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)

EchoServer の例

前の例を簡略化して、リクエストで受信したデータをそのままレスポンスで返すシンプルなエコーサーバーを作成しましょう。必要なのは、前の例と同様に、リクエストストリームからデータを取得して、そのデータをレスポンスストリームに書き込むだけです。

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

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

.listen(8080);

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

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

それ以外の場合は、単純に 404 を返します。

javascript
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)

NOTE

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

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

javascript
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 ステータスコードを送信します。ただし、現実世界のアプリケーションでは、エラーを検査して、正しいステータスコードとメッセージを判断する必要があります。エラーの場合と同様に、エラーのドキュメントを参照する必要があります。

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

javascript
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 リクエストの処理に関する基本事項の大部分を網羅しました。この時点で、次のことができるはずです。

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

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