状況
axiosを使ってAPI通信を試したくて、docker上にlaravelのAPIサーバーを立ち上げてnode.jsの対話モードからaxiosで通信を試したところエラーが返ってきました。
Laravel
route/api.phpの中身は以下
http://localhost/api/test
に接続すると { id: 1, name: 'test' }
のJSONを返す単純なAPIです。
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\ProductController;
Route::get('/test', function (Request $request) {
return response()->json([
'id' => 1,
'name'=> 'test',
]);
});
Laravelと同じコンテナ内でnode.jsを対話モードで起動し、コンソールからaxiosで通信を試みたところエラーが返ってきました
axios.get('http://localhost:8080/api/test');
Promise {
<pending>,
[Symbol(async_id_symbol)]: 406,
[Symbol(trigger_async_id_symbol)]: 6
}
> Uncaught AxiosError: connect ECONNREFUSED ::1:8080
at AxiosError.from (/usr/src/laravel-app/node_modules/axios/dist/node/axios.cjs:841:14)
at RedirectableRequest.handleRequestError (/usr/src/laravel-app/node_modules/axios/dist/node/axios.cjs:3106:25)
at RedirectableRequest.emit (node:events:517:28)
at RedirectableRequest.emit (node:domain:489:12)
at eventHandlers.<computed> (/usr/src/laravel-app/node_modules/follow-redirects/index.js:38:24)
at ClientRequest.emit (node:events:517:28)
at ClientRequest.emit (node:domain:489:12)
at Socket.socketErrorListener (node:_http_client:501:9)
at Socket.emit (node:events:517:28)
at Socket.emit (node:domain:489:12)
at Axios.request (/usr/src/laravel-app/node_modules/axios/dist/node/axios.cjs:4224:41)
at process.processTicksAndRejections (node:internal/process/task_queues:95:5) {
port: 8080,
address: '::1',
syscall: 'connect',
code: 'ECONNREFUSED',
errno: -111,
エラー全文はこちら
Promise { , [Symbol(async_id_symbol)]: 406, [Symbol(trigger_async_id_symbol)]: 6 } > Uncaught AxiosError: connect ECONNREFUSED ::1:8080 at AxiosError.from (/usr/src/laravel-app/node_modules/axios/dist/node/axios.cjs:841:14) at RedirectableRequest.handleRequestError (/usr/src/laravel-app/node_modules/axios/dist/node/axios.cjs:3106:25) at RedirectableRequest.emit (node:events:517:28) at RedirectableRequest.emit (node:domain:489:12) at eventHandlers. (/usr/src/laravel-app/node_modules/follow-redirects/index.js:38:24) at ClientRequest.emit (node:events:517:28) at ClientRequest.emit (node:domain:489:12) at Socket.socketErrorListener (node:_http_client:501:9) at Socket.emit (node:events:517:28) at Socket.emit (node:domain:489:12) at Axios.request (/usr/src/laravel-app/node_modules/axios/dist/node/axios.cjs:4224:41) at process.processTicksAndRejections (node:internal/process/task_queues:95:5) { port: 8080, address: '::1', syscall: 'connect', code: 'ECONNREFUSED', errno: -111, config: { transitional: { silentJSONParsing: true, forcedJSONParsing: true, clarifyTimeoutError: false }, adapter: [ 'xhr', 'http', 'fetch' ], transformRequest: [ [Function: transformRequest] ], transformResponse: [ [Function: transformResponse] ], timeout: 0, xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', maxContentLength: -1, maxBodyLength: -1, env: { FormData: [Function], Blob: [class Blob] }, validateStatus: [Function: validateStatus], headers: Object [AxiosHeaders] { Accept: 'application/json, text/plain, */*', 'Content-Type': undefined, 'User-Agent': 'axios/1.7.2', 'Accept-Encoding': 'gzip, compress, deflate, br' }, method: 'get', url: 'http://localhost:8080/api/test', data: undefined }, request: 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, constructed: true, prefinished: false, errorEmitted: false, emitClose: true, autoDestroy: true, errored: null, closed: false, closeEmitted: false, [Symbol(kOnFinished)]: [] }, _events: [Object: null prototype] { response: [Function: handleResponse], error: [Function: handleRequestError], socket: [Function: handleRequestSocket] }, _eventsCount: 3, _maxListeners: undefined, _options: { maxRedirects: 21, maxBodyLength: Infinity, protocol: 'http:', path: '/api/test', method: 'GET', headers: [Object: null prototype], agents: [Object], auth: undefined, family: undefined, beforeRedirect: [Function: dispatchBeforeRedirect], beforeRedirects: [Object], hostname: 'localhost', port: '8080', agent: undefined, nativeProtocols: [Object], pathname: '/api/test' }, _ended: true, _ending: true, _redirectCount: 0, _redirects: [], _requestBodyLength: 0, _requestBodyBuffers: [], _onNativeResponse: [Function (anonymous)], _currentRequest: ClientRequest { _events: [Object: null prototype], _eventsCount: 7, _maxListeners: undefined, outputData: [], outputSize: 0, writable: true, destroyed: false, _last: true, chunkedEncoding: false, shouldKeepAlive: false, maxRequestsOnConnectionReached: false, _defaultKeepAlive: true, useChunkedEncodingByDefault: false, sendDate: false, _removedConnection: false, _removedContLen: false, _removedTE: false, strictContentLength: false, _contentLength: 0, _hasBody: true, _trailer: '', finished: true, _headerSent: true, _closed: false, socket: [Socket], _header: 'GET /api/test HTTP/1.1\r\n' + 'Accept: application/json, text/plain, */*\r\n' + 'User-Agent: axios/1.7.2\r\n' + 'Accept-Encoding: gzip, compress, deflate, br\r\n' + 'Host: localhost:8080\r\n' + 'Connection: close\r\n' + '\r\n', _keepAliveTimeout: 0, _onPendingData: [Function: nop], agent: [Agent], socketPath: undefined, method: 'GET', maxHeaderSize: undefined, insecureHTTPParser: undefined, joinDuplicateHeaders: undefined, path: '/api/test', _ended: false, res: null, aborted: false, timeoutCb: null, upgradeOrConnect: false, parser: null, maxHeadersCount: null, reusedSocket: false, host: 'localhost', protocol: 'http:', _redirectable: [Circular *1], [Symbol(kCapture)]: false, [Symbol(kBytesWritten)]: 0, [Symbol(kNeedDrain)]: false, [Symbol(corked)]: 0, [Symbol(kOutHeaders)]: [Object: null prototype], [Symbol(errored)]: null, [Symbol(kHighWaterMark)]: 16384, [Symbol(kRejectNonStandardBodyWrites)]: false, [Symbol(kUniqueHeaders)]: null }, _currentUrl: 'http://localhost:8080/api/test', [Symbol(kCapture)]: false }, cause: Error: connect ECONNREFUSED ::1:8080 at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1549:16) at TCPConnectWrap.callbackTrampoline (node:internal/async_hooks:128:17) { errno: -111, code: 'ECONNREFUSED', syscall: 'connect', address: '::1', port: 8080 } }解決策
宛先をlocalhostではなく127.0.0.1にしたら解決しました
axios.get('http://localhost:8080/api/test');
ではなく
axios.get('http://127.0.0.1:8080/api/test');
にしたら通信成功
原因
IPv6で通信しようとしていてできなかった様子
127.0.0.1と明示的にIPv4を使うことで通信できたようです。
以下chatGPTの回答抜粋
エラーメッセージから、::1というIPv6アドレスが使用されていることがわかります。IPv4アドレス127.0.0.1を使用するように試みます。
エラーの中身を見ていると確かにアドレスが::1
となっています。
address: '::1',
syscall: 'connect',
code: 'ECONNREFUSED',
errno: -111,
なぜIPv6が優先して使われたのか、またなぜIPv6だと通信できないのかなどについては深堀りできていません。
通信できたので取り合えずOKとします。
補足ですが、dockerコンテナ内の/etc/hostの中身は以下でした
# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to /etc/wsl.conf:
# [network]
# generateHosts = false
127.0.0.1 localhost
127.0.1.1 toko202406. toko202406
# The following lines are desirable for IPv6 capable hosts
::1 ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
これを見るとlocalhost→127.0.0.1のIPv4で名前解決されると思うのですが原因不明です。