tkhrsskの日記

技術ネタなど

Node.jsでCORS回避する中継Webサーバを建てる

ローカルのhtmlでAPIを叩いて結果を見れるツールを作りたいときがたまにある。 できればjavascriptでブラウザで見れるようにしたい。

Node.jsの簡素なパッケージだけで実現してみた。

サンプルなので自分自身にスタブ用意しているけど、リクエスト先をCORS設定されているような公開APIに変更すればいい。

やりたいことはシンプルなのに、つまずきポイントがちょいちょい出てくるので残しておくことにする。

特にCORSの回避周りだったり、bodyの非同期受信の処理とか。

ググると使えない古い情報で溢れていたりするので、 できればnodejsとjqueryの非推奨呼び出しとなった歴史も整理していきたい。

web.js

let http = require("http");
let https = require("https");
let server = http.createServer();

server.on("request", function (req, res) {
  console.log(
    "HTTP",
    req.httpVersion,
    req.method,
    req.url,
    req.headers["user-agent"]
  );
  console.log("header: ", req.headers);

  // CORS全許可
  res.setHeader("Access-Control-Allow-Origin", "*");
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, HEAD, OPTIONS");
  res.setHeader("Access-Control-Allow-Headers", "*");

  // ignore url
  if (req.url.indexOf("/favicon.ico") === 0) {
    let response = {};
    res.writeHead(404, { "Content-Type": "application/json" });
    res.write(JSON.stringify(response));
    res.end();
    return;
  }

  // OPTIONSメソッドは無条件で正常応答(エラー応答時のCORSエラーを防ぐため)
  if (req.method === "OPTIONS") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end();
    return;
  }

  body = "";
  req.on("data", (chunk) => {
    body += chunk;
  });
  req.on("end", () => {
    console.log("body: ", body);

    // スタブ応答
    if (req.url.indexOf("/api") === 0) {
      console.log("[API] Called ", req.url);
      let response = {};
      res.writeHead(200, { "Content-Type": "application/json" });
      res.write(JSON.stringify(response));
      res.end();
      return;
    }
    host = "127.0.0.1";
    port = 3000;
    method = req.method;
    path = "/api/";
    content = "json";
    protocol = port == 443 ? https : http;

    let options = {
      host: host,
      port: port,
      method: method,
      path: path,
    };

    let payload = body ? body : null;
    options.headers = {
      "Content-Type": "application/json",
    };

    options.headers["user-agent"] = "API Test Client";
    if (req.headers["authorization"]) {
      options.headers["authorization"] = req.headers["authorization"];
    }

    const reqApi = protocol.request(options, function (resApi) {
      var body = "";
      resApi.on("data", (chunk) => {
        body += chunk;
      });
      resApi.on("end", () => {
        console.log("StatusCode: ", resApi.statusCode);
        console.log("Response Headers: ", resApi.headers);
        console.log("Response Body: ", body);
        res.writeHead(resApi.statusCode, resApi.headers);
        res.write(body);
        res.end();
      });
    });
    reqApi.on("error", function (e) {
      console.log("[ERROR][API]" + e.message);
      let response = {};
      res.writeHead(500, { "Content-Type": "application/json" });
      res.write(JSON.stringify(response));
      res.end();
    });
    if (payload) {
      reqApi.write(payload);
    }
    reqApi.end();
  });
});

console.log("http://127.0.0.1:3000/");
server.listen(3000, "127.0.0.1");

index.html

<!DOCTYPE html>
<html lang="ja">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>index</title>
    <style>
        #result {
            white-space: pre-wrap;
        }
    </style>
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"
        integrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script>
</head>

<body>
    <h1>API Request</h1>
    <div>
        <h2>Request</h2>
        Url:<br>
        <select id="requestUrl">
            <option>http://127.0.0.1:3000/</option>
            <option>http://127.0.0.1:3000/api/</option>
            <option>https://www.githubstatus.com/api/v2/summary.json</option>
            <!-- <option>https://www.jma.go.jp/bosai/forecast/data/forecast/140000.json</option> -->
        </select><br>
        Bearer Token:<br>
        <input id="token" type="text" size="64" placeholder="token" value=""><br>
        Payload:<br>
        <textarea id="payload" cols="64" rows="5" placeholder="payload"></textarea></br>
        <button id="run">RUN</button>
        <h2>Response</h2>
        <pre id="status"></pre>
        <pre id="result"></pre>
        <pre id="responsetime"></pre>
    </div>
    <script>
        $('#run').on('click', function () {
            var token = $('#token').val();
            var requestUrl = $('#requestUrl').val();
            var payload = $('#payload').val();
            console.log(payload);
            console.log(token);
            var options = {
                url: requestUrl,
                type: 'get',
                dataType: 'json',
                contentType: 'application/json',
            };
            if (token) {
                options.headers = {
                    'Authorization': 'Bearer ' + token
                };
            }
            if (payload) {
                options.data = payload;
            }
            console.log({ requestUrl, token });
            $('#responsetime').text("");
            $('#status').text("");
            $('#result').text("");
            const startTime = Date.now(); // 開始時間
            $.ajax(options)
                .done(function (data, textStatus, jqXHR ) {
                    const endTime = Date.now(); // 終了時間
                    console.log(JSON.stringify(data));
                    $('#status').text("status: " + jqXHR.status);
                    $('#result').text(JSON.stringify(data));
                    $('#responsetime').text("ResponseTime: " + (endTime - startTime) + " ms");
                })
                .fail(function (jqXHR, textStatus, errorThrown) {
                    const endTime = Date.now(); // 終了時間
                    console.log("fail", jqXHR, textStatus, errorThrown);
                    $('#responsetime').text("ResponseTime: " + (endTime - startTime) + " ms");
                    $('#status').text("status: " + jqXHR.status);
                    $('#result').text("fail");
                    console.log(jqXHR.responseText);
                    $('#result').text(jqXHR.responseText);
                });
        });
    </script>
</body>

</html>