HTTP の HEAD メソッドで外部 API 死活チェックを軽量に行えたメモ
HTTP の HEAD メソッドで外部 API 死活チェックを軽量に行えたメモです。
背景
public-apis のような API のリンク切れを確認したい時があります。教材で紹介するときに切れてたら嫌じゃないですか。
GET リクエストでのチェックを最初考えましたが、これだとレスポンスボディ全体をダウンロードするので、それなりにネットワーク負荷が高く複数チェックするには不向きでした。
解決策
レスポンスボディ全体をダウンロードしないやり方で解決しないかなあというところで HTTP の HEAD メソッドはヘッダーだけ返し、ボディをダウンロードせずステータスコードとリダイレクト先は同じように取得できるため、死活確認に最適よさそうなのでやってみました。
判定ロジックはこんな感じ。
- 2xx → 生存
- 3xx でリダイレクト後ドメインが変わる → ドメイン転用の可能性
- 4xx / 5xx → つながらない・存在しない
- タイムアウト → つながらない(タイムアウト)
コード(Node.js 標準ライブラリのみ)
以下をファイルに保存して、
const https = require('https');
const http = require('http');
const { URL } = require('url');
// チェック対象 URL
const URLS = [
{ name: 'dog.ceo', url: 'https://dog.ceo/dog-api/' },
{ name: 'wttr.in', url: 'https://wttr.in/Tokyo?format=j2' },
{ name: 'zoo-animals', url: 'https://zoo-animal-api.herokuapp.com/' },
{ name: 'sunrise', url: 'https://api.sunrise-sunset.org/json?lat=35.6&lng=139.7' },
{ name: 'dead-api', url: 'https://goweather.xyz/' },
{ name: 'shibe.online', url: 'http://shibe.online/' }
];
function checkUrl(name, url, timeoutMs = 8000) {
const originalHost = (() => {
try { return new URL(url).hostname; } catch { return ''; }
})();
return new Promise((resolve) => {
const done = (status, finalUrl, verdict, note) =>
resolve({ name, url, status, finalUrl, verdict, note });
const attempt = (target, depth = 0) => {
if (depth > 5) return done(null, target, 'error', 'too many redirects');
let parsed;
try { parsed = new URL(target); }
catch { return done(null, target, 'error', 'invalid url'); }
const mod = parsed.protocol === 'https:' ? https : http;
const req = mod.request({
hostname: parsed.hostname,
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
path: parsed.pathname + parsed.search,
method: 'HEAD',
headers: { 'User-Agent': 'url-checker/1.0' },
timeout: timeoutMs,
}, (res) => {
res.destroy(); // ボディは読まない
const code = res.statusCode;
// リダイレクトを手動追跡(location ヘッダーを見る)
if (code >= 301 && code <= 308 && res.headers.location) {
try {
return attempt(new URL(res.headers.location, target).href, depth + 1);
} catch { return done(code, target, 'error', 'bad redirect'); }
}
if (code >= 400) return done(code, target, 'dead', `HTTP ${code}`);
// リダイレクト後にドメインが変わっていたら suspect
if (parsed.hostname !== originalHost)
return done(code, target, 'redirect', `→ ${parsed.hostname}`);
done(code, target, 'ok', '');
});
req.on('timeout', () => { req.destroy(); done(null, target, 'timeout', ''); });
req.on('error', (e) => done(null, target, 'error', e.message.slice(0, 60)));
req.end();
};
attempt(url);
});
}
// 並行チェック
(async () => {
const results = await Promise.all(URLS.map(({ name, url }) => checkUrl(name, url)));
const ICON = { ok: '✅', dead: '❌', redirect: '⚠️ ', timeout: '⏱️ ', error: '❌' };
for (const r of results) {
const icon = ICON[r.verdict] || '?';
const note = r.note ? ` (${r.note})` : '';
console.log(`${icon} [${String(r.status ?? '--').padStart(3)}] ${r.name}${note}`);
}
})();
以下で実行します。
node check.js
このような結果が出ました!
node .\check.js
✅ [200] dog.ceo
✅ [200] wttr.in
❌ [404] zoo-animals (HTTP 404)
✅ [200] sunrise
❌ [404] dead-api (HTTP 404)
❌ [ --] shibe.online (socket hang up)
うまくいってますね。shibe.online は実際 2026 年 8 月現在アクセスできないどころか、より不穏なリダイレクトになっているんですが、うまく検出できてます。
学びとしては以下のようなことがありました。
- HEAD リクエストのリダイレクトは http.request が自動追跡しない(follow_redirects がない)ため、自前で再帰的に追跡する必要がある
- res.destroy() でレスポンスボディの受信を明示的に打ち切る。これをしないとコネクションが残る
- timeout は http.request のオプションで指定できるが、発火時に req.destroy() を呼ばないとソケットが残る
- サーバーによっては HEAD を 405 Method Not Allowed で弾く場合があるので、その場合は GET にフォールバックするか 405 を「生存だが HEAD 非対応」として扱ったほうがよさそう(上記では未実装)
あとは、並行数はなるべく少な目(10くらい)を推奨します。小分けにしてやりましょう。大量 URL を一気に投げると IP ブロックされる可能性もあるので慎重に扱ってください。