HTTPのupgrade headerと、なぜそれがNext.js+Nginxな構成に必要なのか[備忘録]

ふと前に書いた[この記事](https://qiita.com/tatmius/items/a942907b529040eab794)のnginxのreverse proxyのconfigを見ていて見つけたupgrade headerについての備忘録です。

:::note info
 記事後半のNext.jsの部分は物凄く書きかけです。
:::


## そもそもupgrade headerとは
HTTP/1.1で使用される、protocolを変更するためのheader。HTTP/1.1からHTTP/2とか、HTTP/1.1からwebSocketとか、通信の途中でprotocolを変更する為に使用される。

:::note warn
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から提示されたっぽい。

https://nextjs.org/docs/pages/building-your-application/upgrading/version-12#hmr-connection-now-uses-a-websocket

> 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:
>```Nginx
>.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は以下のブログがとても分かりやすかった。

https://blog.jakoblind.no/webpack-hmr/

## Next.jsのコードを追ってみる
HMRがdevサーバーを立ち上げた時などに使用できることから、`next start`コマンドの実装を見れば実はwebpackとか出てくるのでは？という軽い気持ちで[packages/next/src/cli/next-start.ts](https://github.com/vercel/next.js/blob/canary/packages/next/src/cli/next-start.ts)を見てみると、

```typescript
  await startServer({
    dir,
    isDev: false,
    isExperimentalTestProxy,
    hostname: host,
    port,
    keepAliveTimeout,
  })
```
という箇所があり、これがサーバーを起動する関数らしい。
この関数は、[packages/next/src/server/lib/start-server.ts](https://github.com/vercel/next.js/blob/canary/packages/next/src/cli/next-start.ts)で定義されている。

その中に気になる箇所を見つけた。以下である。
```typescript
  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`は、上記コードのすぐ上で定義されており、

```typescript
const server = selfSignedCertificate
    ? https.createServer(
        {
          key: fs.readFileSync(selfSignedCertificate.key),
          cert: fs.readFileSync(selfSignedCertificate.cert),
        },
        requestListener
      )
    : http.createServer(requestListener)
```
となっている。証明書の有無でhttpで建てるかhttpsで建てるかを分岐している。
この`http`と`https`はこのファイルの先頭で以下のようにimportされている。
```typescript
import http from 'http'
import https from 'https'
```

どうやらこれは、Node.jsの組み込み関数らしい。
[node/lib/http.js](https://github.com/nodejs/node/blob/main/lib/http.js)で以下のように定義されていた。
```javascript
/**
 * 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](https://github.com/nodejs/node/blob/main/lib/_http_server.js)で定義されている。
```javascript
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を組み合わせてるんだろうか、とか妄想しています。

今度また調べて追記します。