HTTP の HEAD メソッドで外部 API 死活チェックを軽量に行えたメモ

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 ブロックされる可能性もあるので慎重に扱ってください。