HTTP トランザクションの解剖
このガイドの目的は、Node.js の HTTP 処理のプロセスに関する確固たる理解を与えることです。言語やプログラミング環境に関係なく、HTTP リクエストの仕組みを大まかに理解していることを前提とします。また、Node.js の EventEmitter と Stream にも多少の familiarity があることを前提とします。あまり慣れていない場合は、それぞれの API ドキュメントを簡単に読んでおく価値があります。
サーバーの作成
どんな Node ウェブサーバーアプリケーションも、ある時点でウェブサーバーオブジェクトを作成する必要があります。これはcreateServer
を使用して行います。
const http = require('node:http')
const server = http.createServer((request, response) => {
// magic happens here!
})
createServer
に渡される関数は、そのサーバーに対して行われたすべての HTTP リクエストごとに 1 回呼び出されるため、リクエストハンドラーと呼ばれます。実際、createServer
によって返される Server オブジェクトは EventEmitter であり、これはサーバーオブジェクトを作成し、後でリスナーを追加するという簡略表記に過ぎません。
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 では、便利なプロパティをリクエストオブジェクトに配置することで、これを比較的簡単にしています。
const { method, url } = request
リクエストオブジェクトはIncomingMessage
のインスタンスです。ここのメソッドは常に通常の HTTP メソッド/動詞です。url は、サーバー、プロトコル、ポートを除いた完全な URL です。一般的な URL の場合、これは 3 番目のスラッシュ以降を含みます。
ヘッダーもすぐ近くに存在します。それらはリクエスト上のheaders
という独自のオブジェクト内にあります。
const { headers } = request
const userAgent = headers['user-agent']
ここで重要なのは、クライアントが実際にどのように送信したかに関係なく、すべてのヘッダーが小文字のみで表現されることです。これにより、どのような目的であってもヘッダーを解析する作業が簡素化されます。
ヘッダーが繰り返される場合、ヘッダーに応じて、値が上書きされるか、カンマ区切りの文字列として結合されます。場合によってはこれが問題になる可能性があるため、rawHeaders
も使用できます。
リクエストボディ
POST または PUT リクエストを受信する際、リクエストボディはアプリケーションにとって重要になる可能性があります。リクエストヘッダーへのアクセスよりも、ボディデータの取得は少し複雑です。ハンドラーに渡されるリクエストオブジェクトは、ReadableStream
インターフェースを実装します。このストリームは、他のストリームと同様に、リスンしたり、他の場所にパイプしたりできます。ストリームの 'data'
と 'end'
イベントをリスンすることで、ストリームから直接データを取得できます。
各 'data'
イベントで発行されるチャンクは Buffer
です。文字列データであることがわかっている場合、最適な方法はデータを配列に収集し、'end'
で連結して文字列化することです。
let body = []
request.on('data', chunk => {
body.push(chunk)
})
request.on('end', () => {
body = Buffer.concat(body).toString()
// この時点で、'body' にはリクエストボディ全体が文字列として格納されています
})
NOTE
これは少し面倒に思えるかもしれませんが、多くの場合そうです。幸いなことに、npm には concat-stream
や body
のようなモジュールがあり、このロジックの一部を隠すのに役立ちます。この道に進む前に、何が起こっているのかを十分に理解することが重要であり、それがあなたがここにいる理由です!
エラーに関する簡単なこと
リクエストオブジェクトは ReadableStream
なので、EventEmitter
でもあり、エラーが発生したときに同様に動作します。
リクエストストリームのエラーは、ストリームで 'error'
イベントを発行することで発生します。そのイベントのリスナーがない場合、エラーはスローされ、Node.js プログラムがクラッシュする可能性があります。したがって、ログに記録して続行する場合でも、リクエストストリームに 'error'
リスナーを追加する必要があります。(ただし、何らかの HTTP エラーレスポンスを送信する方がおそらく最善です。これについては後で詳しく説明します。)
request.on('error', err => {
// エラーメッセージとスタックトレースを 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));
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
プロパティを設定します。
response.statusCode = 404 // リソースが見つからないことをクライアントに伝えます。
すぐにわかるように、これにはいくつかのショートカットがあります。
レスポンスヘッダーの設定
ヘッダーは、setHeader
という便利なメソッドを使用して設定されます。
response.setHeader('Content-Type', 'application/json')
response.setHeader('X-Powered-By', 'bacon')
レスポンスでヘッダーを設定する場合、その名前の大文字と小文字は区別されません。ヘッダーを繰り返し設定した場合、最後に設定した値が送信される値になります。
ヘッダーデータの明示的な送信
これまで説明してきたヘッダーとステータスコードの設定方法は、「暗黙的なヘッダー」を使用することを前提としています。これは、本文データの送信を開始する前に、ノードが適切なタイミングでヘッダーを送信してくれることを期待していることを意味します。
必要に応じて、レスポンスストリームにヘッダーを明示的に書き込むことができます。これを行うには、writeHead
というメソッドがあり、ステータスコードとヘッダーをストリームに書き込みます。
ヘッダーデータの明示的な送信
response.writeHead(200, {
'Content-Type': 'application/json',
'X-Powered-By': 'bacon',
})
ヘッダーを設定したら(暗黙的か明示的かに関わらず)、レスポンスデータの送信を開始できます。
レスポンスボディの送信
レスポンスオブジェクトは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>')
NOTE
本文データのチャンクの書き込みを開始する前に、ステータスとヘッダーを設定することが重要です。これは、HTTP レスポンスではヘッダーが本文の前に来るため、理にかなっています。
エラーに関するもう一つの簡単なこと
レスポンスストリームは 'error' イベントも発生させる可能性があり、いずれはそれにも対処する必要があります。リクエストストリームエラーに関するすべてのアドバイスは、ここでも同様に適用されます。
すべてをまとめる
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)
EchoServer の例
前の例を簡略化して、リクエストで受信したデータをそのままレスポンスで返すシンプルなエコーサーバーを作成しましょう。必要なのは、前の例と同様に、リクエストストリームからデータを取得して、そのデータをレスポンスストリームに書き込むだけです。
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 を返します。
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
を使用して、一方から他方へデータを転送できます。これは、エコーサーバーに必要なまさにそれです!
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
に出力するだけです。
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 は他にも多くの機能を提供しているので、EventEmitters
、Streams
、およびHTTP
の API ドキュメントをよく読んでください。