fetchの呼び出し @Javascript & Node.js 実験室

fetchは、HTTP呼び出しで標準的に使っていますが、Content-Typeに従った呼び出し方をいつも忘れてしまうので、備忘録として残しておきます。

実動作の確認のためのソースコードを以下のGitHubに上げておきました。
　https://github.com/poruruba/fetch_laboratory 

Javascriptからの呼び出しを中心に示しますが、同じコードがそのままNode.jsでも動作します。
ちょっとだけ、Lambdaでの動作も示しておきます。

# 呼び出し方法の種類

今回扱う呼び出し方法は以下の通りです。

・Get呼び出し
　HTMLのページ取得でおなじみです。パラメータをQueryStringに指定します。例えば、以下のような呼び出しです。
　`http://localhost:10080/api?param1=abcd`

・Post（Content-Type: application/json）呼び出し
　パラメータをBody部にJSONで指定するWebAPI呼び出しでは一番一般的ですね。

・Post（Content-Type: application/x-www-form-urlencoded）呼び出し
　フォームでSumitするときの方法です。例えば、以下のような呼び出しです。
　`<form action="http://localhost:10080/post-urlencoded" method="post">`

・Post（Content-Type: multipart/form-data）呼び出し
　フォームでmultipart指定でSumitするときの方法です。例えば、以下のような呼び出しです。
　`<form action="http://localhost:10080/post-formdata" method="post" enctype="multipart/form-data">`

# Javascriptの場合

まずは、Javascriptから。

## Post（Content-Type: application/json）呼び出し

```js
function do_post(url, body) {
  const headers = new Headers({ "Content-Type": "application/json" });

  return fetch(url, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: headers
    })
    .then((response) => {
      if (!response.ok)
        throw new Error('status is not 200');
      return response.json();
    });
}
```

Javascriptから送信
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a0b5cd8f-e286-1168-13ed-6c8816ebd639.png)

## Post（Content-Type: application/x-www-form-urlencoded）呼び出し 

```js

function do_post_urlencoded(url, params) {
  const headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' });
  var body = new URLSearchParams(params);

  return fetch(url, {
      method: 'POST',
      body: body,
      headers: headers
    })
    .then((response) => {
      if (!response.ok)
        throw new Error('status is not 200');
      return response.json();
    })
}
```

フォームから送信した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/ff548817-335b-412f-77ca-a5bc5406de10.png)

Javascriptから送信した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/eb2a103e-c7f4-9b5e-cecf-c372a74f164d.png)

## Post（Content-Type: multipart/form-data）呼び出し

```js
function do_post_formdata(url, params){
    var body = Object.entries(params).reduce( (l, [k,v])=> { l.append(k, v); return l; }, new FormData());

  return fetch(url, {
      method : 'POST',
      body: body,
  })
  .then((response) => {
    if( !response.ok )
      throw new Error('status is not 200');
    return response.json();
  });
}
```

フォームから送信した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/9c23a807-ac88-2a1f-6bba-f99f0a1dd97d.png)

Javascriptから送信した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/915f596f-4464-012d-f85e-60595756007c.png)


## GET呼び出し

```js
function do_get(url, qs) {
  var params = new URLSearchParams(qs);

  var params_str = params.toString();
  var postfix = (params_str == "") ? "" : ((url.indexOf('?') >= 0) ? ('&' + params_str) : ('?' + params_str));
  return fetch(url + postfix, {
      method: 'GET',
    })
    .then((response) => {
      if (!response.ok)
        throw new Error('status is not 200');
      return response.json();
    });
}
```

フォームから送信した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a8050262-c38f-7747-44c1-2831ed2ca798.png)

Javascriptから送信した場合
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/ef1cc7de-e679-a265-703f-a7e5b91d845e.png)

## まとめ

まとめるとこんな感じ

```js
// input: url, method, qs, body, params, response_type, content_type, token, api_key
async function do_http(input){
  const method = input.method ? input.method : "POST";
  const content_type = input.content_type ? input.content_type : "application/json";
  const response_type = input.response_type ? input.response_type : "json";

  const headers = new Headers();
  if( content_type != "multipart/form-data" )
    headers.append("Content-Type", content_type);
  if( input.token )
    headers.append("Authorization", "Bearer " + input.token);
  if( input.api_key )
    headers.append("x-api-key", input.api_key);

  let body;
  if( content_type == "application/json" ){
    body = JSON.stringify(input.body);
  }else if( content_type == "application/x-www-form-urlencoded"){
    body = new URLSearchParams(input.params);
  }else if( content_type == "multipart/form-data"){
    body = Object.entries(input.params).reduce((l, [k, v]) => { l.append(k, v); return l; }, new FormData());
  }

  const params = new URLSearchParams(input.qs);
  var params_str = params.toString();
  var postfix = (params_str == "") ? "" : ((input.url.indexOf('?') >= 0) ? ('&' + params_str) : ('?' + params_str));

  return fetch(input.url + postfix, {
    method: method,
    body: body,
    headers: headers
  })
  .then((response) => {
    if (!response.ok)
      throw new Error('status is not 200');

    if( response_type == "json" )
      return response.json();
    else if( response_type == 'blob')
      return response.blob();
    else if( response_type == 'file'){
      const disposition = response.headers.get('Content-Disposition');
      let filename = "";
      if( disposition ){
        filename = disposition.split(/;(.+)/)[1].split(/=(.+)/)[1];
        if (filename.toLowerCase().startsWith("utf-8''"))
            filename = decodeURIComponent(filename.replace(/utf-8''/i, ''));
        else
            filename = filename.replace(/['"]/g, '');
      }
      return response.blob()
      .then(blob =>{
        return new File([blob], filename, { type: blob.type })      
      });
    }
    else if( response_type == 'binary')
      return response.arrayBuffer();
    else // response_type == "text" )
      return response.text();
  });
}
```

# Node.jsの場合

Node.jsでも呼び出し方は同じです。
ポイントは、以下のnpmモジュールを利用するところです。

・node-fetch
　https://github.com/node-fetch/node-fetch
・form-data
　https://github.com/form-data/form-data

以下を先頭に記述することで、ほぼJavascriptと同じ記述で同じ動作となります。

```
const FormData = require('form-data');
const { URL, URLSearchParams } = require('url');
const fetch = require('node-fetch');
const Headers = fetch.Headers;
```

# 実験のためのサンプルページ

以下のようなページから、各呼び出し方法を確認します。
F11を押して、DevToolsを開くと、通信内容を詳しく見ることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/c95f6ca2-22d8-098e-2f0a-65331a77fd2b.png)

先ほど示した関数群を呼び出しているだけでして、詳細はGitHubのソースコードを参照ください。
参考までに、リクエストを受け付けるサーバ側のソースも示しておきます。

```js:index.js
'use strict';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const Redirect = require(HELPER_BASE + 'redirect');

// Lambda＋API Gatewayの場合に必要
//const { URLSearchParams } = require('url');
//const multipart = require('aws-lambda-multipart-parser');

exports.handler = async (event, context, callback) => {
  if( event.path == '/post-json'){
    console.log(event.body);

    var body = JSON.parse(event.body);
    var response = {
      path : event.path,
      param: {
        param1: body.param1,
        param2: body.param2,
      }
    };
    return new Response(response);
  }else
  if( event.path == '/post-urlencoded'){
    // Lambda＋API Gatewayの場合はこちら
    //var body = {};
    //for( var pair of new URLSearchParams(event.body).entries() ) body[pair[0]] = pair[1];
    // swagger_nodeの場合はこちら
    var body = JSON.parse(event.body);

    var response = {
      path : event.path,
      param: {
        param1: body.param1,
        param2: body.param2,
      }
    };
    return new Response(response);
  }else
  if( event.path == '/post-formdata' ){
    // Lambda＋API Gatewayの場合はこちら
    //var body = multipart.parse(event);
    // swagger_nodeの場合はこちら
    var body = JSON.parse(event.body);

    console.log(body);
    var response = {
      path : event.path,
      param: {
        param1: body.param1,
        param2: body.param2,
      }
    };
    return new Response(response);
  }else
  if( event.path == '/get-qs'){
    console.log(event.queryStringParameters);

    var response = {
      path : event.path,
      param: {
        param1: event.queryStringParameters.param1,
        param2: event.queryStringParameters.param2,
      }
    };
    return new Response(response);
  }
};
```

一方で、Node.jsでの呼び出しも確認したかったので、Node.js側の確認のためのソースも示しておきます。
動作としては、いったん、サーバ側でリクエストを受け付けたのち、パラメータで指定されたbody.typeの値によって、各呼び出し方法に従った呼び出しをNode.jsで行っています。
コードを見ていただくと、Javascriptでの呼び出しと同じであることがわかります。

```js

'use strict';

const HELPER_BASE = process.env.HELPER_BASE || '../../helpers/';
const Response = require(HELPER_BASE + 'response');
const Redirect = require(HELPER_BASE + 'redirect');

const FormData = require('form-data');
const fetch = require('node-fetch');
const { URL, URLSearchParams } = require('url');
const Headers = fetch.Headers;

const baseurl = 'http://localhost:10080';

exports.handler = async (event, context, callback) => {
  if( event.path == '/node'){
    console.log(event.body);

    var body = JSON.parse(event.body);
    if( body.type == "post-json"){
      var params = {
        param1: body.param1,
        param2: body.param2,
      };
      return do_post(baseurl + '/post-json', params)
        .then(json => {
          console.log(json);
          return new Response({ type: body.type, resposne: json });
        })
        .catch(error => {
          console.log(error);
          return new Response(error);
        });
    }else
    if( body.type == "post-urlencoded"){
      var params = {
        param1: body.param1,
        param2: body.param2,
      };
      return do_post_urlencoded(baseurl + '/post-urlencoded', params)
        .then(json => {
          console.log(json);
          return new Response({ type: body.type, resposne: json });
        })
        .catch(error => {
          console.log(error);
          return new Response(error);
        });
    }else
    if( body.type == "post-formdata"){
      var params = {
        param1: body.param1,
        param2: body.param2,
      };
      return do_post_formdata(baseurl + '/post-formdata', params)
        .then(json => {
          console.log(json);
          return new Response({ type: body.type, resposne: json });
        })
        .catch(error => {
          console.log(error);
          return new Response(error);
        });
    }else
    if( body.type == 'get-qs'){
      var qs = {
        param1: body.param1,
        param2: body.param2,
      }
      return do_get(baseurl + '/get-qs', qs)
        .then(json => {
          console.log(json);
          return new Response({ type: body.type, resposne: json });
        })
        .catch(error => {
          console.log(error);
          return new Response(error);
        });
    }

    var response = {
      type : body.type,
      param: {
        param1: body.param1,
        param2: body.param2,
      }
    };
    return new Response(response);
  }
};


function do_post(url, body) {
  const headers = new Headers({ "Content-Type": "application/json; charset=utf-8" });

  return fetch(url, {
      method: 'POST',
      body: JSON.stringify(body),
      headers: headers
    })
    .then((response) => {
      if (!response.ok)
        throw new Error('status is not 200');
      return response.json();
    });
}

function do_get(url, qs) {
  var params = new URLSearchParams(qs);

  return fetch(url + `?` + params.toString(), {
      method: 'GET',
    })
    .then((response) => {
      if (!response.ok)
        throw new Error('status is not 200');
      return response.json();
    });
}

function do_post_urlencoded(url, params) {
  const headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' });
  var body = new URLSearchParams(params);

  return fetch(url, {
      method: 'POST',
      body: body,
      headers: headers
    })
    .then((response) => {
      if (!response.ok)
        throw new Error('status is not 200');
      return response.json();
    })
}

function do_post_formdata(url, params) {
  var body = Object.entries(params).reduce((l, [k, v]) => {
    l.append(k, v);
    return l;
  }, new FormData());

  return fetch(url, {
      method: 'POST',
      body: body,
    })
    .then((response) => {
      if (!response.ok)
        throw new Error('status is not 200');
      return response.json();
    });
}
```

# 補足

※レスポンスの型
最近レスポンスはJSONで返されることが多くなっていますので、すべてJSONで返ってくることを想定しています。もし、単なるテキストで返ってくる場合には以下の部分を変更します。

`(変更前) return response.json();`
→
`(変更後) return response.text();`

以上