【node.js】ゼロからWebサーバを作ってみる

普段node.jsでwebサーバを作るときはexpress等のライブラリを使うかと思いますが
以前の記事で自前で作った流れで、もう少しサーバらしくしてみました。

まずは公式サイトからコピペ

↓ここにコードがある。
Node.js® とは
これをそのままコピペする

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

少しいじってメソッド化する

これをちょっといじって、Hello Worldの部分だけカスタマイズできるメソッドを作ります。

// webサーバライブラリ
function createServer(
  action
) {
  const http = require('http');

  const hostname = '127.0.0.1';
  const port = 3000;

  const server = http.createServer((req, res) => {
    action(req, res);
  });

  server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
  });
}

// 使う側
createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

これでcreateServerの引数を変えればレスポンスをカスタマイズできるようになりました。
めでたし、めでたし。

これで終わりではない

サーバを作るってそんな簡単話じゃないです。
このままだと、使う側のメソッド内で例外が発生するとサーバごと落ちます。
とりあえず何が何でも落ちないようにします。

キャッチされてない例外を捻り潰す

// webサーバライブラリ
function createServer(
  action
) {
  const http = require('http');

  const hostname = '127.0.0.1';
  const port = 3000;

  const server = http.createServer((req, res) => {
    action(req, res);
  });

  server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
  });

  // ★追加★
  process.on('uncaughtException', (e) => console.log(e));
}

// 使う側
createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

uncaughtExceptionイベントを拾うようにしました。
uncaughtExceptionイベントは例外が誰にもキャッチされないときに呼ばれます。
それを拾うと、例外のスローが止まって落ちなくなります。
よし。よし。

よくない

このままだとリクエストに対するレスポンスが呼ばれてないので、リクエスト投げた側はサーバに繋ぎっぱなしになります。
サーバのタイムアウト時間が2分なので、実際の挙動としては2分後に切れる感じになります。

安直にトライキャッチするのはダメ

action(req, res)で例外が発生するのでそれをtry-catchで囲んでみる

const server = http.createServer((req, res) => {
  try {
    action(req, res);
  } catch(e) {
    res.statusCode = 500;
    res.end(e);
  }
});

一見これで良さそうですが、全然ダメです。
なぜならnode.jsは非同期処理への考慮が漏れているからです。
たとえば使う側でこんなコードを書いたらキャッチできないです。

createServer((req, res) => {
  setTimeout(() => {
    throw 'Error!!';
  }, 1000);
});

非同期処理での例外はdomainでキャッチする

非同期処理で例外はdomainを使います。

const domain = require('domain');
const server = http.createServer((req, res) => {
  var d = domain.create();
  d.on('error', (e) => {
    res.statusCode = 500;
    res.end(e);
  });
  d.run(() => action(req, res));
});

これでrun()の中で起きた例外は非同期処理であってもon()で拾うことができます。
このタイミングでレスポンスを返せばOK

例外時に落ちずにエラーを返すWebサーバ

てことで完成です。

// webサーバライブラリ
function createServer(
  action
) {
  const http = require('http');
  const domain = require('domain');

  const hostname = '127.0.0.1';
  const port = 3000;

  const server = http.createServer((req, res) => {
    var d = domain.create();
    // 例外を拾ってエラーで返す
    d.on('error', (e) => {
      res.statusCode = 500;
      res.end(e);
    });
    d.run(() => action(req, res));
  });

  server.listen(port, hostname, () => {
    console.log(`Server running at http://${hostname}:${port}/`);
  });

  // 何が何でも落とさない
  process.on('uncaughtException', (e) => console.log(e));
}

// 使う側
createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

公式サイトのコードに加えた機能は以下の通りです。

  • 例外発生時に何が何でも落ちないようにした
  • 例外発生時にレスポンスをエラーで返すようにした

まとめ

いつもexpressに任せっきりの処理を自作してみました。
ライブラリのありがたみを感じました。
プロダクトを自作ライブラリで作るのはありえないと思いました。