Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8691

axiosのerror handling

$
0
0
はじめに JavaScript(Node.js)でHTTP通信をする上で便利なライブラリであるaxiosであるが、そのエラーハンドリングで躓いた。 また、axiosのエラーハンドリング(axios error handling)でググってもconsole.log()で出力させているようなものばかりで実際にプロダクトでは(自作のちょっとしたやつでも)使えなさそう1なものが多くヒットしたので、自身で開発する中で学んだ事を備忘録として残しておく。 ※中にはerror.resposeをすればよいというものもあり、その覚え方ではマズいのでそこもちゃんと整理したい。 axiosとは axiosの仕組みを少し見てみる事で、axiosのerror handlingを理解する error handlingを考える対象のソースコード 以下のようなソースコードで、errorHandler(res, error)の実装をする場面を考える。 server.js const express = require('express') const app = express() /* Middleware */ app.use(express.urlencoded({extended: false})); app.use(express.json()); // Cors for cross origin allowance const cors = require('cors'); app.use(cors()); // dotenv const dotenv = require('dotenv') dotenv.config(); app.listen(8081, () => console.log('listening on port 8081!')) const axios = require('axios').default; const instance = axios.create({ baseURL: 'https://api.countrystatecity.in/v1/', timeout: 2000, headers: { 'X-CSCAPI-KEY': `${process.env.COUNTRYSTATECITY_API_KRY}` } }) app.get('/allCountries', async (req, res) => { try { const countries = await instance.get('countries') res.send({ countries: countries.data }) } catch (error) { errorHandler(res, error) } }) const errorHandler = (res, error) => { // ここを実装したい } axiosのinterfaceを見て理解する VS codeでaxiosのgetを参照すると以下のソースが見れる。 index.d.ts export interface AxiosInstance { // 省略 get<T = any, R = AxiosResponse<T>>(url: string, config?: AxiosRequestConfig): Promise<R>; // 省略 } ここからHTTP通信が成功すればAxiosResponseインスタンス(オブジェクト)が返ってくる事が分かる。 ※ちなみに、AxiosResponseは以下のように定義されているので、axios.get('/hoge').then((res) => { console.log(res.data) })やaxios.get('/hoge').then((res) => { console.log(res.status) })などでそのオブジェクト・値が取得できる。 index.d.ts export interface AxiosResponse<T = any> { data: T; status: number; statusText: string; headers: any; config: AxiosRequestConfig; request?: any; } では、エラーの時はどうなるかというと、エラーの際は以下のAxiosErrorインスタンス(オブジェクト)が返ってくる事が分かる。 これはさらにErrorインターフェースを継承しており、そのErrorインターフェースは次のように定義されている。 index.d.ts export interface AxiosError<T = any> extends Error { config: AxiosRequestConfig; code?: string; request?: any; response?: AxiosResponse<T>; isAxiosError: boolean; toJSON: () => object; } lib.es5.d.ts interface Error { name: string; message: string; stack?: string; } つまり、AxiosErrorインターフェースに基づくインスタンス(オブジェクト)(実際にエラーの際に返ってくるオブジェクト)には、 Error : name, message, stack AxiosError : config, code, request, response, isAxiosError, toJSON というkeyが存在する事が分かる。 エラーになる場合を場合分けしてみる 単純にエラーと言っても以下のように2パターンあるように思える。 パターン 概要 詳細 1 responseがある(undefinedでない) APIのrequestは有効でresponseも返ってくるがHTTP statusが200でない 2 responseがundefined APIのrequestは有効だが何らかの理由でresponseが返ってこないそもそもAPIをcallしようとしたがその前にエラー つまり、AxiosErrorのresponseがundefinedになる・ならないの2つで場合分けが必要という事。 ではerrorHandler(res, error)の実装はどうなるのか? 上記のインターフェースの定義+2パターンある事を踏まえて、以下のようにすれば全てのエラーパターンで対応できる。 error-handling.js const errorHandler = (res, error) => { if (error.response) { res.status(error.response.status).send({ error: error.response.data, errorMsg: error.message }) } else { res.status(500).send({ errorMsg: error.message }) } } AxiosErrorのresponseがundefinedでない場合、responseはAxiosResponseインターフェースに合致するオブジェクトになるのでerror.response.dataでWeb APIから返ってきたエラー内容を取得できるまた、AxiosResponseにはstatusもあるのでそれをres.status(error.response.status)のようにして返す事ができる※フロントエンドでaxiosを使っている時は、画面上でエラーに対するトースターなどを表示する時に使う事になる AxiosErrorのresponseがundefinedである場合、error.response.hogeはエラーになる代わりにErrorインターフェースで持っているmessageを利用し何が起きたのかを把握できるようにerror.messageでエラーメッセージを取得する おまけ ググるとよく出てくるやつ axios-error-handling axios.get('/user/12345') .catch(function (error) { if (error.response) { // The request was made and the server responded with a status code // that falls out of the range of 2xx console.log(error.response.data); console.log(error.response.status); console.log(error.response.headers); } else if (error.request) { // The request was made but no response was received // `error.request` is an instance of XMLHttpRequest in the browser and an instance of // http.ClientRequest in node.js console.log(error.request); } else { // Something happened in setting up the request that triggered an Error console.log('Error', error.message); } console.log(error.config); }); これのようにconsole.log()を出しているだけで、これではプロダクトの開発はできない。 単純にconsole.log()を res.status(error.response.status).send(error.response.data) res.status(500).send(error.request) のように置き換えても、1はうまくいくが少なくとも2はうまくいかない。 2がうまくいかないのは2のerror.requestはJSONに変換できるものではないからであるが、それは以下の実際のerrorの中身を全て書き出しているセクションを参照。 エラー時にaxiosが返すオブジェクト パターン1 responseがある(undefinedでない) 以下のエラーは、先ほどerrorHandler(res, error)を実装しようとしていたソースコードの const instance = axios.create({ baseURL: 'https://api.countrystatecity.in/v1/', timeout: 2000, headers: { 'X-CSCAPI-KEY': `${process.env.COUNTRYSTATECITY_API_KRY}` } }) の部分でheaders: { 'X-CSCAPI-KEY': ${process.env.COUNTRYSTATECITY_API_KRY} }をコメントアウトした時に発生したエラー。 この場合は、HTTP Requestは正常にできているのでresponseがundefinedになっておらずerror.responseでAxiosResponseの中身を取得できることが分かる。 Error: Request failed with status code 401 at createError (C:\Users\user\OneDrive\ドキュメント\travel-app\node_modules\axios\lib\core\createError.js:16:15) at settle (C:\Users\user\OneDrive\ドキュメント\travel-app\node_modules\axios\lib\core\settle.js:17:12) at IncomingMessage.handleStreamEnd (C:\Users\user\OneDrive\ドキュメント\travel-app\node_modules\axios\lib\adapters\http.js:260:11) at IncomingMessage.emit (events.js:327:22) at endReadableNT (internal/streams/readable.js:1327:12) at processTicksAndRejections (internal/process/task_queues.js:80:21) { config: { url: 'countries', method: 'get', headers: { Accept: 'application/json, text/plain, */*', 'User-Agent': 'axios/0.21.1' }, baseURL: 'https://api.countrystatecity.in/v1/', transformRequest: [ [Function: transformRequest] ], transformResponse: [ [Function: transformResponse] ], timeout: 3000, adapter: [Function: httpAdapter], xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', maxContentLength: -1, maxBodyLength: -1, validateStatus: [Function: validateStatus], data: undefined }, request: <ref *1> ClientRequest { _events: [Object: null prototype] { socket: [Function (anonymous)], abort: [Function (anonymous)], aborted: [Function (anonymous)], connect: [Function (anonymous)], error: [Function (anonymous)], timeout: [Function (anonymous)], prefinish: [Function: requestOnPrefinish] }, _eventsCount: 7, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: true, chunkedEncoding: false, shouldKeepAlive: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: false, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: 0, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: TLSSocket { _tlsOptions: [Object], _secureEstablished: true, _securePending: false, _newSessionPending: false, _controlReleased: true, secureConnecting: false, _SNICallback: null, servername: 'api.countrystatecity.in', alpnProtocol: false, authorized: true, authorizationError: null, encrypted: true, _events: [Object: null prototype], _eventsCount: 10, connecting: false, _hadError: false, _parent: null, _host: 'api.countrystatecity.in', _readableState: [ReadableState], _maxListeners: undefined, _writableState: [WritableState], allowHalfOpen: false, _sockname: null, _pendingData: null, _pendingEncoding: '', server: undefined, _server: null, ssl: [TLSWrap], _requestCert: true, _rejectUnauthorized: true, parser: null, _httpMessage: [Circular *1], [Symbol(res)]: [TLSWrap], [Symbol(verified)]: true, [Symbol(pendingSession)]: null, [Symbol(async_id_symbol)]: 14, [Symbol(kHandle)]: [TLSWrap], [Symbol(kSetNoDelay)]: false, [Symbol(lastWriteQueueSize)]: 0, [Symbol(timeout)]: null, [Symbol(kBuffer)]: null, [Symbol(kBufferCb)]: null, [Symbol(kBufferGen)]: null, [Symbol(kCapture)]: false, [Symbol(kBytesRead)]: 0, [Symbol(kBytesWritten)]: 0, [Symbol(connect-options)]: [Object], [Symbol(RequestTimeout)]: undefined }, _header: 'GET /v1/countries HTTP/1.1\r\n' + 'Accept: application/json, text/plain, */*\r\n' + 'User-Agent: axios/0.21.1\r\n' + 'Host: api.countrystatecity.in\r\n' + 'Connection: close\r\n' + '\r\n', _keepAliveTimeout: 0, _onPendingData: [Function: noopPendingOutput], agent: Agent { _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, defaultPort: 443, protocol: 'https:', options: [Object], requests: {}, sockets: [Object], freeSockets: {}, keepAliveMsecs: 1000, keepAlive: false, maxSockets: Infinity, maxFreeSockets: 256, scheduling: 'fifo', maxTotalSockets: Infinity, totalSocketCount: 1, maxCachedSessions: 100, _sessionCache: [Object], [Symbol(kCapture)]: false }, socketPath: undefined, method: 'GET', maxHeaderSize: undefined, insecureHTTPParser: undefined, path: '/v1/countries', _ended: true, res: IncomingMessage { _readableState: [ReadableState], _events: [Object: null prototype], _eventsCount: 3, _maxListeners: undefined, socket: [TLSSocket], httpVersionMajor: 1, httpVersionMinor: 1, httpVersion: '1.1', complete: true, headers: [Object], rawHeaders: [Array], trailers: {}, rawTrailers: [], aborted: false, upgrade: false, url: '', method: null, statusCode: 401, statusMessage: 'Unauthorized', client: [TLSSocket], _consuming: true, _dumped: false, req: [Circular *1], responseUrl: 'https://api.countrystatecity.in/v1/countries', redirects: [], [Symbol(kCapture)]: false, [Symbol(RequestTimeout)]: undefined }, aborted: false, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null, reusedSocket: false, host: 'api.countrystatecity.in', protocol: 'https:', _redirectable: Writable { _writableState: [WritableState], _events: [Object: null prototype], _eventsCount: 2, _maxListeners: undefined, _options: [Object], _ended: true, _ending: true, _redirectCount: 0, _redirects: [], _requestBodyLength: 0, _requestBodyBuffers: [], _onNativeResponse: [Function (anonymous)], _currentRequest: [Circular *1], _currentUrl: 'https://api.countrystatecity.in/v1/countries', _timeout: Timeout { _idleTimeout: -1, _idlePrev: null, _idleNext: null, _idleStart: 3203, _onTimeout: null, _timerArgs: undefined, _repeat: null, _destroyed: true, [Symbol(refed)]: true, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 21, [Symbol(triggerId)]: 18 }, [Symbol(kCapture)]: false }, [Symbol(kCapture)]: false, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype] { accept: [Array], 'user-agent': [Array], host: [Array] } }, response: { status: 401, statusText: 'Unauthorized', headers: { date: 'Mon, 31 May 2021 14:01:16 GMT', 'content-type': 'application/json', 'transfer-encoding': 'chunked', connection: 'close', 'cache-control': 'no-cache, private', 'access-control-allow-origin': '*', 'cf-cache-status': 'DYNAMIC', 'cf-request-id': '0a6453a3a00000fcd9700ea000000001', 'expect-ct': 'max-age=604800, report-uri="https://report-uri.cloudflare.com/cdn-cgi/beacon/expect-ct"', 'report-to': '{"endpoints":[{"url":"https:\\/\\/a.nel.cloudflare.com\\/report\\/v2?s=gzML4CT7VVjmC4HaF70JwTZ2wpR9dEItbXiXcIMSg5%2BqgegCBsN4sa9QJwHQbRrd2MuvR%2BBDODUmnVhB%2B%2B9DF0q4ncmwRLKCRRqtQNzJHwI70jiBcmOUBrCk2E%2BOSmXsk9W%2Fsz0%3D"}],"group":"cf-nel","max_age":604800}', nel: '{"report_to":"cf-nel","max_age":604800}', server: 'cloudflare', 'cf-ray': '6580bbb29aeffcd9-KIX', 'alt-svc': 'h3-27=":443"; ma=86400, h3-28=":443"; ma=86400, h3-29=":443"; ma=86400' }, config: { url: 'countries', method: 'get', headers: [Object], baseURL: 'https://api.countrystatecity.in/v1/', transformRequest: [Array], transformResponse: [Array], timeout: 3000, adapter: [Function: httpAdapter], xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', maxContentLength: -1, maxBodyLength: -1, validateStatus: [Function: validateStatus], data: undefined }, request: <ref *1> ClientRequest { _events: [Object: null prototype], _eventsCount: 7, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: true, chunkedEncoding: false, shouldKeepAlive: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: false, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: 0, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [TLSSocket], _header: 'GET /v1/countries HTTP/1.1\r\n' + 'Accept: application/json, text/plain, */*\r\n' + 'User-Agent: axios/0.21.1\r\n' + 'Host: api.countrystatecity.in\r\n' + 'Connection: close\r\n' + '\r\n', _keepAliveTimeout: 0, _onPendingData: [Function: noopPendingOutput], agent: [Agent], socketPath: undefined, method: 'GET', maxHeaderSize: undefined, insecureHTTPParser: undefined, path: '/v1/countries', _ended: true, res: [IncomingMessage], aborted: false, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null, reusedSocket: false, host: 'api.countrystatecity.in', protocol: 'https:', _redirectable: [Writable], [Symbol(kCapture)]: false, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype] }, data: { error: "Unauthorized. You shouldn't be here." } }, isAxiosError: true, toJSON: [Function: toJSON] } パターン2 responseがない(undefined) 以下のエラーは、先ほどerrorHandler(res, error)を実装しようとしていたソースコードの const instance = axios.create({ baseURL: 'https://api.countrystatecity.in/v1/', timeout: 2000, headers: { 'X-CSCAPI-KEY': `${process.env.COUNTRYSTATECITY_API_KRY}` } }) の部分でtimeout: 2000をtimeout: 500にした時に発生したエラー。 見ていくと分かるが、requestはあるがresponseがundefinedになっており、error.responseをreturnしても意味ないのは実際に中身を見てみても分かる。 ※ググるとよく出てくるやつで単純にerror.requestをres.status(500).send({ error: error.request })のように返そうとしてもエラーになると言ったが、これは以下のrequestキーの中身を見ると分かる通り、requestは<ref *1> Writable {…となっておりJSONに変換できるオブジェクトではないから。 Error: timeout of 500ms exceeded at createError (C:\Users\user\OneDrive\ドキュメント\travel-app\node_modules\axios\lib\core\createError.js:16:15) at RedirectableRequest.handleRequestTimeout (C:\Users\user\OneDrive\ドキュメント\travel-app\node_modules\axios\lib\adapters\http.js:280:16) at RedirectableRequest.emit (events.js:315:20) at Timeout._onTimeout (C:\Users\user\OneDrive\ドキュメント\travel-app\node_modules\follow-redirects\index.js:165:12) at listOnTimeout (internal/timers.js:554:17) at processTimers (internal/timers.js:497:7) { config: { url: 'countries', method: 'get', headers: { Accept: 'application/json, text/plain, */*', 'X-CSCAPI-KEY': '***************************************', ←マスキングしてます 'User-Agent': 'axios/0.21.1' }, baseURL: 'https://api.countrystatecity.in/v1/', transformRequest: [ [Function: transformRequest] ], transformResponse: [ [Function: transformResponse] ], timeout: 500, adapter: [Function: httpAdapter], xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', maxContentLength: -1, maxBodyLength: -1, validateStatus: [Function: validateStatus], data: undefined }, code: 'ECONNABORTED', request: <ref *1> Writable { _writableState: WritableState { objectMode: false, highWaterMark: 16384, finalCalled: false, needDrain: false, ending: false, ended: false, finished: false, destroyed: false, decodeStrings: true, defaultEncoding: 'utf8', length: 0, writing: false, corked: 0, sync: true, bufferProcessing: false, onwrite: [Function: bound onwrite], writecb: null, writelen: 0, afterWriteTickInfo: null, buffered: [], bufferedIndex: 0, allBuffers: true, allNoop: true, pendingcb: 0, prefinished: false, errorEmitted: false, emitClose: true, autoDestroy: true, errored: null, closed: false }, _events: [Object: null prototype] {}, _eventsCount: 0, _maxListeners: undefined, _options: { maxRedirects: 21, maxBodyLength: 10485760, protocol: 'https:', path: '/v1/countries', method: 'GET', headers: [Object], agent: undefined, agents: [Object], auth: undefined, hostname: 'api.countrystatecity.in', port: null, nativeProtocols: [Object], pathname: '/v1/countries' }, _ended: true, _ending: true, _redirectCount: 0, _redirects: [], _requestBodyLength: 0, _requestBodyBuffers: [], _onNativeResponse: [Function (anonymous)], _currentRequest: ClientRequest { _events: [Object: null prototype], _eventsCount: 1, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: true, _last: true, chunkedEncoding: false, shouldKeepAlive: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: false, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, _contentLength: 0, _hasBody: true, _trailer: '', finished: true, _headerSent: true, socket: [TLSSocket], _header: 'GET /v1/countries HTTP/1.1\r\n' + 'Accept: application/json, text/plain, */*\r\n' + 'X-CSCAPI-KEY: *************************************\r\n' + 'User-Agent: axios/0.21.1\r\n' + 'Host: api.countrystatecity.in\r\n' + 'Connection: close\r\n' + '\r\n', _keepAliveTimeout: 0, _onPendingData: [Function: noopPendingOutput], agent: [Agent], socketPath: undefined, method: 'GET', maxHeaderSize: undefined, insecureHTTPParser: undefined, path: '/v1/countries', _ended: false, res: null, aborted: true, timeoutCb: null, upgradeOrConnect: false, parser: [HTTPParser], maxHeadersCount: null, reusedSocket: false, host: 'api.countrystatecity.in', protocol: 'https:', _redirectable: [Circular *1], [Symbol(kCapture)]: false, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype] }, _currentUrl: 'https://api.countrystatecity.in/v1/countries', _timeout: Timeout { _idleTimeout: 500, _idlePrev: null, _idleNext: null, _idleStart: 3646, _onTimeout: [Function (anonymous)], _timerArgs: undefined, _repeat: null, _destroyed: true, [Symbol(refed)]: true, [Symbol(kHasPrimitive)]: false, [Symbol(asyncId)]: 41, [Symbol(triggerId)]: 38 }, [Symbol(kCapture)]: false }, response: undefined, isAxiosError: true, toJSON: [Function: toJSON] } 参考文献 https://www.yoheim.net/blog.php?q=20170801 axiosのcatchでerror objectの中身を見れない ブラウザ・サーバに限らず、プロダクトを作る上ではconsole.log()に出してもそれの情報に基づいた処理ができないので意味ない ↩

Viewing all articles
Browse latest Browse all 8691

Trending Articles