5.

PHP file_get_contents「503 Service

編集
この記事の要点
  • 原因①: 対象サーバーが過負荷 / メンテで 本当に 503(一時的、時間を置く)
  • 原因②: Bot 検出(Cloudflare / Akamai 等)。User-Agent 未指定の file_get_contents はブロック対象になりやすい
  • 原因③: レート制限。同一 IP からの高頻度アクセスで一時的に 503
  • 対処: stream_context_createUser-Agent / Referer を指定、またはcURL に切替、リトライ実装
  • 長期的には Guzzle / Http ファサードでリトライ・指数バックオフ・タイムアウトを統一管理

エラーの典型例

Warning: file_get_contents(https://example.com/api):
  failed to open stream: HTTP request failed!
  HTTP/1.1 503 Service Unavailable
in /var/www/html/fetch.php on line 12

file_get_contents()fopen() で外部 URL を読もうとした際、HTTP ステータスが 503 で返ったときの警告です。「ストリームを開けなかった」と書かれていますが、原因はHTTP レイヤの 503 です。

原因の切り分け

原因判定方法主な対処
① 一時的なメンテ・過負荷ブラウザで開いてみる、curl コマンドで再現時間を置く、相手に問い合わせ
② Bot 検出(Cloudflare 等)ブラウザは OK でスクリプトのみ 503、レスポンスに cf-ray ヘッダUser-Agent / Referer / Cookie を設定
③ レート制限連続アクセス時のみ発生間隔を空ける、指数バックオフ
④ DNS 障害・SSL 問題エラー文言を確認DNS / 証明書を確認
⑤ 自サーバ側ファイアウォール同じ URL を別ホストから叩くと OK送信元 IP / IPS を確認

対処1: User-Agent を指定する(最頻出)

file_get_contents のデフォルト UA は PHP/8.x となり、これだけで多くの CDN が Bot 判定で 503 を返します。stream_context_create で UA とヘッダを補います:

$context = stream_context_create([
    'http' => [
        'method'  => 'GET',
        'header'  => implode("\r\n", [
            'User-Agent: Mozilla/5.0 (compatible; MyBot/1.0; +https://example.com/bot)',
            'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
            'Accept-Language: ja,en;q=0.9',
            'Referer: https://example.com/',
        ]),
        'timeout' => 10,
        'ignore_errors' => true,   // 4xx/5xx でも例外的に body を取りたい場合
    ],
    'ssl' => [
        'verify_peer'      => true,
        'verify_peer_name' => true,
    ],
]);

$body = @file_get_contents('https://example.com/api', false, $context);

// $http_response_header に HTTP ヘッダ一覧が入る
// 例: HTTP/1.1 503 Service Unavailable
if (isset($http_response_header[0])
    && preg_match('#\bHTTP/\S+ (\d+)#', $http_response_header[0], $m)
) {
    $status = (int) $m[1];
    if ($status >= 400) {
        throw new RuntimeException("HTTP $status: $body");
    }
}

対処2: cURL に切り替える(強く推奨)

file_get_contents はリトライ・タイムアウト制御・詳細エラーが弱いです。外部 HTTP は cURL かライブラリ経由に切り替えるのが本筋:

function fetch(string $url): string
{
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_USERAGENT      => 'Mozilla/5.0 MyBot/1.0',
        CURLOPT_HTTPHEADER     => [
            'Accept: text/html,*/*;q=0.8',
            'Accept-Language: ja,en;q=0.9',
        ],
    ]);
    $body = curl_exec($ch);
    if ($body === false) {
        $err = curl_error($ch);
        curl_close($ch);
        throw new RuntimeException("cURL: $err");
    }
    $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new RuntimeException("HTTP $status returned");
    }
    return $body;
}

対処3: リトライ + 指数バックオフ

503 はしばしば一時的なので、少し待って再試行すれば成功します。指数バックオフ(1s → 2s → 4s …)で実装するのが定石:

function fetchWithRetry(string $url, int $maxRetry = 4): string
{
    $attempt = 0;
    while (true) {
        $attempt++;
        try {
            return fetch($url);
        } catch (RuntimeException $e) {
            if (strpos($e->getMessage(), 'HTTP 503') === false
             && strpos($e->getMessage(), 'HTTP 429') === false) {
                throw $e;  // 503 / 429 以外はリトライしない
            }
            if ($attempt >= $maxRetry) throw $e;
            $sleep = min(2 ** ($attempt - 1), 30);  // 1, 2, 4, 8, ... 最大 30 秒
            error_log("HTTP 503: retry in {$sleep}s (attempt $attempt)");
            sleep($sleep);
        }
    }
}

対処4: Retry-After ヘッダを尊重する

多くのサーバは 503 / 429 と一緒に Retry-After ヘッダで「N 秒後に再試行してね」を伝えます:

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HEADER         => true,
]);
$response = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$hSize  = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$rawH   = substr($response, 0, $hSize);
$body   = substr($response, $hSize);
curl_close($ch);

if ($status === 503 || $status === 429) {
    if (preg_match('/^Retry-After:\s*(\d+)/mi', $rawH, $m)) {
        $wait = (int) $m[1];
        sleep(min($wait, 60));
        // 再試行
    }
}

対処5: allow_url_fopen が無効化されている可能性

共有レンタルサーバや一部の本番環境では allow_url_fopen = Off になっています。この場合 file_get_contents 自体が URL を開けず、別エラーになります:

ini_get('allow_url_fopen');   // "1" なら有効

// 確認用 PHP コマンド
// php -i | grep allow_url_fopen

無効ならば cURL 一択です。OS / ホスティング会社の設定で変更不可のことが多いため、移植時の前提として扱います。

対処6: Laravel / Guzzle で書き直す

// Laravel
use Illuminate\Support\Facades\Http;

$response = Http::withHeaders([
        'User-Agent' => 'MyApp/1.0',
    ])
    ->timeout(15)
    ->retry(3, 1000)   // 1 秒間隔で 3 回
    ->get('https://example.com/api');

if ($response->successful()) {
    $data = $response->json();
}

// Guzzle
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;

$stack = HandlerStack::create();
$stack->push(Middleware::retry(
    function ($retries, $request, $response = null, $exception = null) {
        return $retries < 3 && $response && $response->getStatusCode() === 503;
    },
    function ($retries) {
        return 1000 * (2 ** $retries);   // 指数バックオフ (ms)
    }
));
$client = new Client(['handler' => $stack, 'timeout' => 15]);
$res = $client->get('https://example.com/api');

本当に「相手が落ちている」場合の見分け方

# ターミナルから直接 curl
curl -v -A "Mozilla/5.0" https://example.com/api

# ステータスだけ
curl -s -o /dev/null -w "%{http_code}\n" -A "Mozilla/5.0" https://example.com/api

# 別ホストから(自宅 / モバイル回線 / クラウド)でも 503 なら相手側障害
# 自サーバからだけ 503 なら IP ブロック / FW / Bot 検知

FAQ

Q: 警告だけ消したい
A: @file_get_contents() で抑制可能だが、根本対処にならない。ステータスを $http_response_header で確認して例外化するべき。

Q: Cloudflare の保護を突破できる?
A: 規約上の問題が出るため避けるべき。相手に API 連携を依頼するか、相手が提供する正規エンドポイントを使う。

Q: 503 が永続的に出続ける
A: 相手側が IP ブロックしている可能性。送信元 IP の見直し、もしくは契約サポート窓口へ問い合わせ。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. Error 400 : Bad Request Parameter Operation is missing
  2. MissingParameter. The request must contain the parameter Signature.
  3. RequestThrottled. AWS Access Key ID: ... . You are submitting requests too quickly
  4. SignatureDoesNotMatch. The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.
  5. failed to open stream: HTTP request failed! HTTP/1.1 503 Service Unavailable