Overview
Fetch APIを当たり前に使うわけですが、これタイムアウトって何秒なんだろう?という疑問からタイムアウトを実装することにしました。
タイムアウトがないとユーザーがブラウザ上でいつまでも応答を受け取れなかったり、クラウド上だとタイムアウトまで終了せずにコストが増大します
そんなことにならないよう、調査した結果を載せておきます。
Target reader
- Fetch APIでタイムアウトや中断の方法を知りたい方。
Prerequisite
- ブラウザはIEを除いた主要ブラウザとする。(IEだとFetch APIがないからpolyfill...)
- Node.jsのバージョンはV10系とする。
Body
ブラウザ編
MDNでFetch APIのタイムアウトのオプションを探してみるが見つからない。
そんな中、取得の中止というものが見つかる。
https://developer.mozilla.org/ja/docs/Web/API/Fetch_API#Concepts_and_usage
取得の中止
ブラウザーは Fetch や XHR などの操作を完了前に中止させることができる AbortController および AbortSignal インターフェイス(つまり Abort API)に実験的に対応し始めています。詳しくはインターフェイスのページを参照してください。
AbortSignalのページには中止ボタンを押下したときに、通信を中断するサンプルコードがある。
https://developer.mozilla.org/ja/docs/Web/API/AbortSignal
また、動画のダウンロードを中断するDemoページのリンクもあるので、デベロッパーツールでネットワークを見ると中断ボタンで確かに中断することが確認できる。
https://mdn.github.io/dom-examples/abort-api/
さらにこのページのリンクから「Abort signals and fetch」というタイトルで、タイムアウトの実装コードも掲載されている。
https://developers.google.com/web/updates/2017/09/abortable-fetch
私がReactで使っている15秒でタイムアウトするコードも恐縮ながら掲載しておく
コード中のfetchのオプションであるsignal
についてはFetch APIのページを見ると、中断のための入り口が設けられていることがわかる。
https://developer.mozilla.org/ja/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Syntax
asyncfunctionfetchCore(url,option={}){// set timeout after 15sconstcontroller=newAbortController();consttimeout=setTimeout(()=>{controller.abort()},option.timeout||15000);try{constresponse=awaitfetch(url,{signal:controller.signal,// for timeout...option,});if(!response.ok){constdescription=`status code:${response.status} , text:${response.statusText}`;thrownewError(description);}returnresponse;}finally{clearTimeout(timeout);}}exportasyncfunctionfetchText(url,option){constresponse=awaitfetchCore(url,option);returnawaitresponse.text();}exportdefault{fetchText};
Node.js編
ブラウザは自前でタイマーの設定が必要になるが、無事にタイムアウトで終了させれることを確認。
今度はサーバーサイドのNode.jsではどうするかということを考えていく。
Node.jsはFetch APIが標準ではないため、node-fetchというパッケージを利用することになる。
node-fetchには以下のような文章がある。
https://github.com/node-fetch/node-fetch#features
Useful extensions such as timeout, redirect limit, response size limit, explicit errors for troubleshooting.
タイムアウト等の便利な拡張機能だと!!!
https://github.com/node-fetch/node-fetch#options
timeout: 0, // req/res timeout in ms, it resets on redirect. 0 to disable (OS limit applies). Signal is recommended instead.
timeoutがありながらSignalを利用することをお勧めする…だと( ゚д゚)ポカーン
timeoutを作っていたけどブラウザのFetch APIがAbortController使ったタイムアウト可能になったからそっちに寄せたのかな?
sample.jsconstfetch=require('node-fetch');constAbortController=require('abort-controller');constcontroller=newAbortController();consttimeout=setTimeout(()=>{controller.abort();},150);fetch('https://example.com',{signal:controller.signal}).then(res=>res.json()).then(data=>{useData(data);},err=>{if(err.name==='AbortError'){console.log('request was aborted');}}).finally(()=>{clearTimeout(timeout);});
これってほぼブラウザのFetchと一緒じゃないですか
結果としては、Node.jsにesmパッケージを導入すれば、Node.jsでもimport/exportが利用でき、ブラウザとコードを共通化できる。
ただし、Node.jsにはfetchやAbortControllerがビルトインされていないため、以下のようにnode-fetch
とabort-controller
をインストール必要がある。
逆に言えばブラウザのコードの最上部に二つのパッケージを取り込めばそれがNode.jsのコードになる!
constfetch=require('node-fetch');constAbortController=require('abort-controller');// 以下はブラウザ編のソースコードそのまま
おまけにjsdom編
jsdomはNode.jsでスクレイピングするときに便利なもので、読み込んだHTMLファイルのDOM操作をエミュレートして、ブラウザ上と同じようなことができたりする。
jsdomはfromURL()
というメソッドがあり、urlを指定するとDomのPromiseを返してくれる。
このメソッドのタイムアウトはどうやって設定すればいいか調査してみた。
2016年のissueへの回答を引用する。
https://github.com/jsdom/jsdom/issues/596
The best way is to make the request yourself, then feed the resulting HTML to jsdom.
Google翻訳先生曰く
最善の方法は、リクエストを自分で作成し、結果のHTMLをjsdomにフィードすることです。
おま
ただもう少し調べてみると2019年にtimeout
というオプションを用意していた模様。
なぜかマージされずに放置されているが…時が経てばマージされるはず。
https://github.com/jsdom/jsdom/pull/2488
結局fromURL()
は使わずにFetch APIで取得したHTMLのテキストをJSDOMコンストラクタの引数に入れるのがよさそう。
適所適材ですね。
// htmlはFetch APIで取得したテキストconstdom=newJSDOM(html,{});const{document}=dom.window;
Conclusion
これでもういつまで経っても応答が来ないという事態に遭遇することはないでしょう。
Fetch APIにタイムアウトのオプションが付くのが一番ですが、フロントもバックも同一のコードで済むのでこれはこれでいいかなと思います。
Have a great day!