Node.js で外部ライブラリを使わずに API を叩く

ちょっと API を叩いて、返ってきた json をいじりたいけど、そのために

```bash
$ npm install request
```
なーんて仰々しい事をしたくない時もあるわけで、外部ライブラリを使わずに行こうとしたら思いの外ハマってしまったのでメモ書き。

自分の環境は `v6.9.1`

## TL;DR
`http.request` の `data` イベントで得られるデータは Buffer。そして断片的。
`end` イベントの時にまとめる処理を書かないとハマる。あと `Buffer.concat` も `Array.prototype.concat` みたいな感覚で使おうとするとハマる。というか使えない←
最終的な [snippet はこちら](https://gist.github.com/takayukioda/0c223b23733a6e80b08aac0b6b221b27)

## `http` もしくは `https` モジュールで可能
フロントエンドでは fetch とかいう新機能(?)新規格(?)があると前に読んだけど、探してみたけど、どうも node とは関係のない話らしい。代わりに [http module に `request` なるメソッド](https://nodejs.org/dist/latest-v6.x/docs/api/http.html#http_http_request_options_callback)があるそうな。`http` はその名の通り暗号通信は門外漢なので、暗号通信を使いたい場合は `https` モジュールを使うといい。

## まずは公式ドキュメントのコードをペッて見て書いたコード
コードをきちんと読まずに「あー、callback にイベントハンドラが返ってきてその中で `data` イベントを通して取ってくるのね、オッケー」とか思って書いたのが下のコード。
知ってる人からみたら色々抜けている。でもここまでだと上手く動いてるように見えるからタチが悪い。（一応この時から兆候はあるけど、 json の量が多くて気づけてなかった）

```js
http.request(options, resp => {
  console.log(`status: ${resp.statusCode}`);
  console.log(`headers: ${resp.headers}`);

  resp.on("data", data => {
    console.log(`data: ${data}`);
  });
}).on("error", e => {
  console.log(`Error: ${e}`);
}).end();
```

## そして次のステップから転がるようにハマりだす←
#### 返ってくるのは json だからとコードを書くと SyntaxError とか怒られる。

```js
http.request(options, resp => {
  console.log(`status: ${resp.statusCode}`);
  console.log(`headers: ${resp.headers}`);

  resp.on("data", data => {
    const result = JSON.parse(data);
    /** ↑ parse 時に SyntaxError が起きる (レスポンスが十分小さければ大丈夫かも？)
      * しかも エラーメッセージの一貫で json らしきデータも見えるから「どこで？」となる
      * 実は「このデータ、パース仕切れなかった！」という表示なんだけど、こういう時に限って気づかない
      */
    console.log(result);
  });
}).on("error", e => {
  console.log(`Error: ${e}`);
}).end();
```

#### 何が起きてるんだ！！ と思って console.log すると 16進数の嵐

```js
http.request(options, resp => {
  console.log(`status: ${resp.statusCode}`);
  console.log(`headers: ${resp.headers}`);

  resp.on("data", data => {
    console.log(data);
    /** 出力はこんな感じ <Buffer 48 65 6c 6c 6f 77 20 4f 72 6c 64>
      * 最初のときは `data: ${data}` とやっているから勝手に文字列化されて、
      * 文字列が渡ってたように見えるけど実は Buffer
      */
    const result = JSON.parse(data);
    console.log(result);
  });
}).on("error", e => {
  console.log(`Error: ${e}`);
}).end();
```

#### toString をしてやっと「あれ、これ、断片化されてる？」と気づく

```js
http.request(options, resp => {
  console.log(`status: ${resp.statusCode}`);
  console.log(`headers: ${resp.headers}`);

  resp.on("data", data => {
    console.log(data.toString("utf8"));
    /** ここで下と同じ json が見えて「これはもしや。。。」となる
    const result = JSON.parse(data);
    /** やっぱりここでエラー */
    console.log(result);
  });
}).on("error", e => {
  console.log(`Error: ${e}`);
}).end();
```

#### `end` イベントの存在に気付き直すも Buffer を "byteの配列" と勘違いし怒られる
```js
http.request(options, resp => {
  console.log(`status: ${resp.statusCode}`);
  console.log(`headers: ${resp.headers}`);

  let bufs = [];
  resp.on("data", bytes => {
    bufs = bufs.concat(bytes);
    /** bytes と書いているけど、Buffer は array とは違うので、やっていることは
      * `bufs.push(bytes)` と同じで、 Buffer の入った配列を作っているだけ
      */
  }).on("end", () => {
    const result = JSON.parse(bufs.toString("utf8"));
    /** Buffer が入った配列なのでやっぱり失敗する */
    console.log(result);
  });
}).on("error", e => {
  console.log(`Error: ${e}`);
}).end();
```

#### [Buffer のドキュメント](https://nodejs.org/dist/latest-v6.x/docs/api/buffer.html#buffer_class_method_buffer_concat_list_totallength) で concat の文字を見て喜ぶも concat の使い方を勘違いし、エラー
```js
http.request(options, resp => {
  console.log(`status: ${resp.statusCode}`);
  console.log(`headers: ${resp.headers}`);

  let bufs = Buffer.alloc(0);
  resp.on("data", chunk => {
    bufs = bufs.concat(chunk);
    /** TypeError: a.concat is not a function
      * Buffer の concat は prototype ではなくクラスメソッド `Buffer.concat`
      */
  }).on("end", () => {
    const result = JSON.parse(bufs.toString("utf8"));
    console.log(result);
  });
}).on("error", e => {
  console.log(`Error: ${e}`);
}).end();
```

#### やっと完成
```js
http.request(options, resp => {
  console.log(`status: ${resp.statusCode}`);
  console.log(`headers: ${resp.headers}`);

  let bufs = [];
  resp.on("data", chunk => {
    // まずは全部配列に突っ込む
    // 正直、この時点で chunk の順番が保証されているのか知らない←
    bufs.push(chunk);
  }).on("end", () => {
    // Buffer.concat は Buffer の配列を受け取って、全部を連結した Buffer を返す
    const data = Buffer.concat(buffs).toString("utf8");
    const result = JSON.parse(data);
    console.log(result);
  });
}).on("error", e => {
  console.log(`Error: ${e}`);
}).end();
```

まぁ http で通信するって事は TCP なわけで、そうしたらデータの順序も保証されてるよな。よな？ って感覚で使ってる←

## この実装はどれくらい安全なのだろうか
普通の API はそんなに膨大な量のデータを返すわけじゃないべ？ という前提で作っているけど、データ量が多かったらどうなるんだろうかとかは気になる。
調べるつもりはまだない。

## なにはともあれ外部ライブラリなしで API を叩けることは分かった
ツールを作るとかになったらこれを使うことは無いだろうけど、ちょっとした snippet として覚えておくと良さそう