ふと前に書いたこの記事のnginxのreverse proxyのconfigを見ていて見つけたupgrade headerについての備忘録です。
記事後半のNext.jsの部分は物凄く書きかけです。
そもそもupgrade headerとは
HTTP/1.1で使用される、protocolを変更するためのheader。HTTP/1.1からHTTP/2とか、HTTP/1.1からwebSocketとか、通信の途中でprotocolを変更する為に使用される。
HTTP/2以降ではupgrade
ヘッダーは許可されていません。HTTP/1.1のみで使用できます。
使用の流れ
upgrade
headerはクライアントがサーバーに対してprotocolの変更を要求する際に使用します。
サーバーはupgrade
headerを受け取ると、upgradeするか否かを判断し、upgradeする場合は101 Switching Protocols
を返します。
逆に、サーバーからクライアントにプロトコルのupgradeを要求する場合は、426 Upgrade Required
をヘッダーに入れて返信します。
webSocket APIはその辺全部自動でやってくれるらしい。
webSocket = new WebSocket("ws://destination.server.ext", "optionalProtocol");
なぜNext.jsのreverse proxyでHTTP/1.1の機能を使うようになったの?
以下の version12のリリースの際に HMR connection に webSocket を使用することになり、その際にこのコードブロックがNext.jsから提示されたっぽい。
Previously, Next.js used a server-sent events connection to receive HMR events. Next.js 12 now uses a WebSocket connection.
In some cases when proxying requests to the Next.js dev server, you will need to >ensure the upgrade request is handled correctly. For example, in nginx you would need to add the following configuration:.location /_next/webpack-hmr { proxy_pass http://localhost:3000/_next/webpack-hmr; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }
HMRとは?
HMR(Hot Module Replacement)の事。エディタでファイルを編集していて、保存した後に勝手に表示するコンポーネントも更新してくれるアノ機能。実はwebSocketが使用されているらしい。
webpackのHMRは以下のブログがとても分かりやすかった。
Next.jsのコードを追ってみる
HMRがdevサーバーを立ち上げた時などに使用できることから、next start
コマンドの実装を見れば実はwebpackとか出てくるのでは?という軽い気持ちでpackages/next/src/cli/next-start.tsを見てみると、
await startServer({
dir,
isDev: false,
isExperimentalTestProxy,
hostname: host,
port,
keepAliveTimeout,
})
という箇所があり、これがサーバーを起動する関数らしい。
この関数は、packages/next/src/server/lib/start-server.tsで定義されている。
その中に気になる箇所を見つけた。以下である。
server.on('upgrade', async (req, socket, head) => {
try {
await upgradeHandler(req, socket, head)
} catch (err) {
socket.destroy()
Log.error(`Failed to handle request for ${req.url}`)
console.error(err)
}
})
見るからにwebSocketに関係してそうである。
upgradeHandler
も気になるが、先にserver
を調べる。
このserver
は、上記コードのすぐ上で定義されており、
const server = selfSignedCertificate
? https.createServer(
{
key: fs.readFileSync(selfSignedCertificate.key),
cert: fs.readFileSync(selfSignedCertificate.cert),
},
requestListener
)
: http.createServer(requestListener)
となっている。証明書の有無でhttpで建てるかhttpsで建てるかを分岐している。
このhttp
とhttps
はこのファイルの先頭で以下のようにimportされている。
import http from 'http'
import https from 'https'
どうやらこれは、Node.jsの組み込み関数らしい。
node/lib/http.jsで以下のように定義されていた。
/**
* Returns a new instance of `http.Server`.
* @param {{
* IncomingMessage?: IncomingMessage;
* ServerResponse?: ServerResponse;
* insecureHTTPParser?: boolean;
* maxHeaderSize?: number;
* requireHostHeader?: boolean;
* joinDuplicateHeaders?: boolean;
* highWaterMark?: number;
* rejectNonStandardBodyWrites?: boolean;
* }} [opts]
* @param {Function} [requestListener]
* @returns {Server}
*/
function createServer(opts, requestListener) {
return new Server(opts, requestListener);
}
このserver
というのは、node/lib/_http_server.jsで定義されている。
function Server(options, requestListener) {
if (!(this instanceof Server)) return new Server(options, requestListener);
if (typeof options === 'function') {
requestListener = options;
options = kEmptyObject;
} else if (options == null) {
options = kEmptyObject;
} else {
validateObject(options, 'options');
}
storeHTTPOptions.call(this, options);
net.Server.call(
this,
{ allowHalfOpen: true, noDelay: options.noDelay ?? true,
keepAlive: options.keepAlive,
keepAliveInitialDelay: options.keepAliveInitialDelay,
highWaterMark: options.highWaterMark });
if (requestListener) {
this.on('request', requestListener);
}
// Similar option to this. Too lazy to write my own docs.
// http://www.squid-cache.org/Doc/config/half_closed_clients/
// https://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
this.httpAllowHalfOpen = false;
this.on('connection', connectionListener);
this.on('listening', setupConnectionsTracking);
this.timeout = 0;
this.maxHeadersCount = null;
this.maxRequestsPerSocket = 0;
this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
}
webSocketはどこに行った???
今のところ、Node.jsの組み込みserver(という言い方が正しいのかわからないけど)に、Next.js側でwebSocketを組み合わせてるんだろうか、とか妄想しています。
今度また調べて追記します。