개발을 하다 보면 Docker 컨테이너 내부에서 호스트 머신(PC)의 API를 호출해야 하는 경우가 종종 있습니다.
일반적으로 Docker for Mac/Windows에서는 host.docker.internal
을 사용하지만, Linux 환경에서는 Docker 브리지의 기본 게이트웨이 주소인 172.17.0.1
을 사용하는 경우가 많습니다.
그런데 이 주소로 API를 호출하는 과정에서 매우 이상한 문제를 겪게 되어 트러블슈팅 과정을 공유해보고자 합니다.
증상: curl은 되는데 fetch는 실패
먼저, 컨테이너 내부에서 호스트(172.17.0.1
)로 네트워크 연결이 되는지 확인하기 위해 curl
명령어를 사용했습니다.
eb7b0d89f82b:/app# curl http://172.17.0.1:6000/webhook/.... -v
* Trying 172.17.0.1:6000...
* Connected to 172.17.0.1 (172.17.0.1) port 6000
...
< HTTP/1.1 401 Unauthorized
< WWW-Authenticate: Basic realm="Webhook"
...
Authorization is required!
401 Unauthorized
응답이 왔습니다. 이는 서버가 인증을 요구한다는 뜻으로, 네트워크 연결 자체는 성공했다는 의미입니다.
그런데 동일한 주소를 Node.js의 fetch
API를 사용하여 호출하면 다른 결과가 나옵니다.
// test.js
// (인증 로직은 편의상 생략)
const url = 'http://172.17.0.1:6000/webhook/....';
try {
const response = await fetch(url, { method: 'GET' });
console.log(await response.text());
} catch (error) {
console.error('Error calling webhook:', error.message);
}
eb7b0d89f82b:/app# node test.js
Error calling webhook: fetch failed
분명 curl
로는 연결이 되는데, 왜 Node.js의 fetch
만 fetch failed
라는 네트워크 연결 실패 오류를 반환하는 것일까요?
원인: curl과 fetch는 다르다
이 문제의 핵심은 curl
과 Node.js의 fetch
가 동일하게 작동하지 않는다는 점입니다.
curl
: C언어로 작성된, 매우 표준적이고 검증된 저수준(low-level) HTTP 클라이언트입니다.- Node.js
fetch
: Node.js 18 버전부터 내장된fetch
API는undici
라는 이름의 별도 HTTP 클라이언트 라이브러리를 기반으로 합니다.
즉, curl
이 성공했다는 것은 “네트워크 경로, IP, 포트, 방화벽”은 모두 정상이란 뜻입니다.
fetch failed
가 발생한 이유는, Node.js의 undici
구현체가 특정 Docker 네트워크 환경(이 경우 172.17.0.1
게이트웨이)과 통신하는 과정에서 호환성 문제를 일으켰기 때문입니다. (주로 IPv6/IPv4 처리 방식 차이나 커넥션 풀링 방식의 차이로 인해 발생하곤 합니다)
axios
는 어떨까요?
흥미롭게도, 이 상황에서 axios
라이브러리를 사용하면 대부분 성공합니다.
import axios from 'axios';
// const response = await axios.get(url, { auth: { ... } });
axios
가 성공하는 이유는 undici
와 다르게, Node.js의 내장 http
모듈을 기반으로 작동하기 때문입니다. 이 http
모듈은 curl
과 유사하게 더 고전적이고 단순한 방식으로 통신하므로 호환성 문제를 피할 수 있었던 것입니다.
해결: undici
를 우회하기
원인을 알았으니 해결 방법은 명확합니다. undici
기반의 fetch
대신, http
모듈 기반의 클라이언트를 사용하면 됩니다.
방법 1. axios
라이브러리 사용 (권장)
가장 간단하고 현대적인 방법입니다. npm
이나 yarn
으로 axios
를 설치하고 코드를 수정합니다.
# ESM 환경에서 require 오류가 났던 것을 기억합니다.
npm install axios
// test.js
import axios from 'axios'; // require 대신 import 사용
const url = 'http://172.17.0.1:6000/webhook/....';
// 401 오류를 해결하기 위해 인증 정보 추가
const auth = {
username: 'YOUR_USERNAME',
password: 'YOUR_PASSWORD'
};
try {
// axios는 auth 객체를 Basic 인증 헤더로 자동 변환해 줍니다.
const response = await axios.get(url, { auth: auth });
console.log('Webhook GET call successful!');
console.log('Status Code:', response.status);
console.log('Server response:', response.data);
} catch (error) {
// ... (Axios 오류 처리) ...
console.error('Error calling webhook:', error.message);
}
방법 2. Node.js 내장 http
모듈 사용
axios
같은 외부 라이브러리를 설치하기 어려운 환경이라면, Node.js에 100% 내장된 http
모듈을 직접 사용할 수 있습니다. curl
과 거의 동일하게 작동합니다.
// test.js (Node.js 내장 http 모듈 사용)
import http from 'http'; // ESM 방식
import { URL } from 'url'; // URL 파싱을 위해 추가
const urlString = 'http://172.17.0.1:6000/webhook/....';
// 인증 정보
const credentials = 'YOUR_USERNAME:YOUR_PASSWORD';
const encodedCredentials = Buffer.from(credentials).toString('base64');
const { hostname, port, pathname } = new URL(urlString);
const options = {
hostname: hostname,
port: port,
path: pathname,
method: 'GET',
headers: {
'Authorization': `Basic ${encodedCredentials}`
}
};
const req = http.request(options, (res) => {
console.log(`Status Code: ${res.statusCode}`);
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
console.log('Server response:', data);
});
});
req.on('error', (error) => {
// 이 오류는 'fetch failed'와 동일한 네트워크 오류입니다.
console.error('Error calling webhook:', error.message);
});
req.end();
마무리
fetch failed
라는 단순한 오류 메시지를 만났을 때, 무조건 네트워크 방화벽이나 IP 주소 문제라고 단정 짓기 쉽습니다.
하지만 curl
과 같은 기본 도구로 교차 검증을 해보니, 문제는 네트워크가 아니라 Node.js의 fetch
구현체인 undici
와 특정 Docker 네트워크 환경 간의 호환성 문제임을 알 수 있었습니다.
비슷한 문제를 겪으신다면, fetch
대신 axios
나 http
기본 모듈을 사용해 보시는 것을 추천해 드립니다.