カテゴリー: Uncategorized

  • バーチャルホストごとにApacheのログ出力先を設定する方法

    Apacheでは、バーチャルホストごとにエラーログとアクセスログの出力先を個別に指定することができます。複数のサイトを同一サーバー上で運用する場合、ログを分離管理することで、トラブルシューティングや解析が格段にしやすくなります。


    バーチャルホスト環境でのエラーログ出力の基本

    通常のグローバル設定(httpd.confやapache2.conf)

    まず、Apache全体で共通のログ出力設定がされていることがあります。

    apacheコピーする編集するErrorLog "/var/log/httpd/error_log"
    

    この設定は、バーチャルホストに属さないリクエストや構文エラーが発生した際に使用されるグローバルなエラーログ出力先です。


    バーチャルホストごとのログ設定例

    バーチャルホストを設定する際に、以下のように個別のログファイルを指定することが可能です。

    apacheコピーする編集する<VirtualHost *:80>
        ServerName example.com
        DocumentRoot /var/www/example
    
        # エラーログの保存先
        ErrorLog "/var/log/httpd/example_error.log"
    
        # アクセスログの保存先
        CustomLog "/var/log/httpd/example_access.log" combined
    
        <Directory /var/www/example>
            AllowOverride All
            Require all granted
        </Directory>
    </VirtualHost>
    

    ディレクティブの意味

    ディレクティブ説明
    ErrorLogエラー出力先ファイルを指定します
    CustomLogアクセスログ出力先とフォーマット(例:combined)を指定します
    combinedApache標準のアクセスログ形式です(IPアドレス、User-Agentなどを含む)

    複数サイトをバーチャルホストで管理する例

    apacheコピーする編集する<VirtualHost *:80>
        ServerName site-a.local
        DocumentRoot /var/www/site-a
        ErrorLog /var/log/httpd/site-a_error.log
        CustomLog /var/log/httpd/site-a_access.log combined
    </VirtualHost>
    
    <VirtualHost *:80>
        ServerName site-b.local
        DocumentRoot /var/www/site-b
        ErrorLog /var/log/httpd/site-b_error.log
        CustomLog /var/log/httpd/site-b_access.log combined
    </VirtualHost>
    

    このように設定することで、サイトごとにログファイルが分かれ、原因の特定や解析が容易になります。


    ログ出力先ディレクトリの違い

    ディストリビューションログディレクトリ例
    CentOS / RHEL系/var/log/httpd/
    Debian / Ubuntu系/var/log/apache2/

    各OSの構成に合わせて、ログの保存パスを適切に設定してください。


    Dockerや非rootユーザーで運用する場合の注意点

    Apacheのログ出力先が /var/log/httpd/ などのシステムディレクトリになっていると、Docker内や非rootユーザーでの実行時に書き込み権限のエラーが発生することがあります。

    回避例(ログを /app/logs/ に出力)

    apacheコピーする編集するErrorLog /app/logs/site-error.log
    CustomLog /app/logs/site-access.log combined
    

    このように、コンテナ内の書き込み可能なパスに変更するのがポイントです。


    ログフォーマットをカスタマイズする例

    apacheコピーする編集するLogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" custom_format
    CustomLog /var/log/httpd/custom_access.log custom_format
    

    設定ファイルの保存場所

    OS系統保存先ディレクトリ例
    CentOS / RHEL系/etc/httpd/conf.d/your-site.conf
    Debian / Ubuntu系/etc/apache2/sites-available/your-site.conf

    Ubuntu系でサイト設定を有効化するには:

    bashコピーする編集するsudo a2ensite your-site.conf
    sudo systemctl reload apache2
    

    設定確認コマンド

    bashコピーする編集する# 全バーチャルホストとログの出力先を確認
    apachectl -S
    
    # ErrorLogの設定をgrepで探す
    grep ErrorLog /etc/httpd/conf.d/*.conf
    

    最後に:設定反映と再読み込み

    コマンド内容
    sudo apachectl configtest設定ファイルの文法チェック
    sudo systemctl reload httpdCentOS / RHEL系のApache再読み込み
    sudo systemctl reload apache2Ubuntu / Debian系のApache再読み込み

    まとめ

    状況エラーログの出力先
    バーチャルホストに ErrorLog 設定あり指定されたファイルに出力される
    バーチャルホストに ErrorLog 設定なしグローバル設定のログファイルに出力される
    Apache自体のエラー(構文ミスなど)グローバルエラーログに出力される

    バーチャルホスト単位でログを分けることで、運用管理がしやすくなり、障害対応の精度も向上します。複数のサイトを運用している場合は、ぜひこの方法を取り入れてください。ログディレクトリのパーミッションやApacheのユーザー権限にも注意しながら設定しましょう。

  • シェルスクリプトでログ内の日時を抽出・比較する方法【日付パースと条件分岐】

    シェルスクリプトでログファイルを処理するとき、「指定日時以降のログだけ抽出したい」といったケースは多くあります。この記事では、様々なフォーマットの日付に対応した抽出と比較処理の方法をご紹介します。


    1. 前提:対象となる日時フォーマットの例

    対象となるログの日時には、以下のような形式があります。

    ✅ ISO形式(一般的なログやCSVなど)

    yamlコピーする編集する2025-03-26 14:55:00
    

    ✅ Apache形式のアクセスログ

    csharpコピーする編集する[26/Mar/2025:14:55:00 +0900]
    

    ✅ エラーログ風の形式

    csharpコピーする編集する[Wed Mar 26 14:55:00.123456 2025]
    

    2. 処理方針と全体の流れ

    シェルスクリプトでは、次の手順で日時を処理します:

    1. 正規表現で日付を抽出
    2. 抽出した文字列を UNIXタイム に変換
    3. cutoff(比較対象の基準時間)と比較

    3. パターン別の対応方法とサンプル

    🔹 ① ISO形式(2025-03-26 14:55:00)の場合

    awkコピーする編集するmatch($0, /([0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2})/, m)
    cmd = "date -d \"" m[1] "\" +%s"
    cmd | getline t
    close(cmd)
    if (t >= cutoff) print $0
    

    🔹 ② Apache形式([26/Mar/2025:14:55:00 +0900])の場合

    awkコピーする編集するmatch($0, /\[([0-9]{2}\/[A-Za-z]{3}\/[0-9]{4}):/, m)
    datetime = m[1]
    cmd = "date -d \"" datetime "\" +%s"
    cmd | getline t
    

    Apache形式のように : の前で分割するのがポイントです。


    🔹 ③ エラーログ風([Wed Mar 26 14:55:00.123456 2025])の場合

    awkコピーする編集するmatch($0, /\[[A-Za-z]{3} ([A-Za-z]{3}) ([0-9]{1,2}) ([0-9]{2}:[0-9]{2}:[0-9]{2})\.[0-9]+ ([0-9]{4})\]/, m)
    datetime = m[1] " " m[2] " " m[3] " " m[4]
    cmd = "date -d \"" datetime "\" +%s"
    cmd | getline t
    

    この形式では ミリ秒を無視する のが実用的です。


    4. 基準時間(cutoff)を定義する

    例えば「90日前以降のログを抽出」したい場合は、以下のようにUNIX時間を取得します。

    90日前のcutoff

    bashコピーする編集するCUTOFF=$(date -d "90 days ago" +%s)
    

    1時間前のcutoff

    bashコピーする編集するCUTOFF=$(date -d "1 hour ago" +%s)
    

    5. 構文テンプレート(まとめ)

    以下は、cutoffと日付を比較して抽出する基本テンプレートです。

    bashコピーする編集するCUTOFF=$(date -d "90 days ago" +%s)
    
    awk -v cutoff="$CUTOFF" '
    {
        if (match($0, /ここに正規表現/, m)) {
            cmd = "date -d \"" m[1] "\" +%s"
            cmd | getline t
            close(cmd)
            if (t >= cutoff) {
                print $0
            }
        }
    }
    ' logfile.log
    

    6. まとめ

    処理内容解説
    ログから日時を抽出awk + 正規表現を使用
    日付の比較方法date -d でUNIXタイムに変換して比較
    cutoff(基準時間)date -d "◯日前" +%s で定義
    応用ISO, Apache形式、エラーログ形式にも対応可能

    備考: この処理はログクレンジング、監査用フィルタリング、アーカイブ対象の抽出など、さまざまな場面で活用できます。用途に応じて正規表現とcutoffの条件を調整してください。

  • CentOSでシェルスクリプトをcronで定期実行する方法

    定期的にシェルスクリプト(例:clean_logs.sh)を自動実行させる方法をステップバイステップで解説します。CentOSやWSL2環境にも対応しています。


    1. スクリプトに実行権限を付与する

    まず、作成したシェルスクリプトに実行権限を付けます。

    chmod +x /home/youruser/scripts/clean_logs.sh

    2. スクリプト内のコマンドはフルパスで指定する

    cronでは環境変数が限定されているため、findrmなどのコマンドもフルパスで記述します。

    #!/bin/bash
    LOG_DIR="/var/log/httpd"
    FIND_BIN="/usr/bin/find"
    RM_BIN="/usr/bin/rm"
    
    $FIND_BIN "$LOG_DIR" -name "*.log" -mtime +90 -exec $RM_BIN -f {} \;

    3. crontabに登録する

    次に、crontabにスクリプトを登録します。

    crontab -e

    エディタが開いたら、以下のように記述します。

    0 2 * * * /home/youruser/scripts/clean_logs.sh >> /var/log/clean_logs.log 2>&1

    この例では、毎日2:00にスクリプトを実行し、ログは/var/log/clean_logs.logに出力されます。

    曜日・時間の設定パターン

    フィールド意味設定例
    0〜590(ちょうど0分)
    0〜232(午前2時)
    1〜31*(毎日)
    1〜12*(毎月)
    曜日0〜6(日=0)*(毎日)

    週1回だけ実行したい場合(毎週日曜深夜)

    0 0 * * 0 /home/youruser/scripts/clean_logs.sh >> /var/log/clean_logs.log 2>&1

    4. 登録内容を確認する

    crontab -l

    5. ログ出力場所についての注意点

    cronによる出力は通常、メール送信または/var/log/cronに記録されます。任意のファイルにログを残したい場合は、リダイレクトを使用してください:

    >> /var/log/clean_logs.log 2>&1

    6. WSL2環境での注意点

    WSL2ではcronデーモンが自動起動しないため、手動で起動が必要です。

    # Ubuntu系の場合
    sudo service cron start
    
    # CentOS系の場合
    sudo /etc/init.d/crond start

    起動確認:

    ps aux | grep cron

    WSL2でcronを自動起動したい場合は、.bashrcに記述するなどの対応が必要です。


    まとめ

    やることコマンド・説明
    スクリプトに実行権限を付けるchmod +x script.sh
    crontabを編集するcrontab -e
    フルパス指定で登録する/full/path/to/script.shを記述
    WSL2ではcronの起動が必要sudo /etc/init.d/crond startなどで手動起動
    登録結果を確認するcrontab -l

    設定ファイルやコマンド内容が不安な場合は、確認・アドバイスも可能です。ぜひお気軽にご相談ください。

  • CakePHPでContent-Lengthヘッダーが付かないときの原因と対処法


    CakePHPで開発をしていると、APIレスポンスに Content-Length ヘッダーが付かずに困ることがあります。本記事では、Content-Length が付与される条件や、それを妨げている要因、対策方法を詳しくまとめます。


    Content-Length を付与させる条件と方法

    1. Apache に削除させない

    .htaccess000-default.conf に以下の記述があると、Apacheが Content-Length を明示的に削除してしまいます。不要であれば削除しましょう。

    Header unset Content-Length
    Header set Transfer-Encoding "chunked"
    

    2. Transfer-Encoding: chunked を無効にする

    チャンク転送が有効だと Content-Length は使われません。無効にするには以下のように設定します。

    Header unset Transfer-Encoding
    SetEnv no-gzip 1
    

    3. PHP側で出力サイズを明示する

    単純なPHPスクリプトの場合:

    <?php
    $data = "Hello World!";
    header('Content-Length: ' . strlen($data));
    echo $data;
    

    CakePHPのコントローラーで明示的に設定:

    $this->response = $this->response
        ->withHeader('Content-Length', strlen($this->response->getBody()));
    

    4. output_buffering を有効にする

    php.ini でバッファリングを有効にしておくと、出力サイズを自動検出して Content-Length を付けてくれます。

    output_buffering = 4096
    

    あるいは:

    output_buffering = On
    

    Docker使用時は /usr/local/etc/php/php.ini などの場所にも注意してください。

    5. mod_deflate / gzip の影響を防ぐ

    圧縮が有効だと Transfer-Encoding に変わり Content-Length が除外されることがあります。

    SetEnv no-gzip 1
    

    チェックコマンド一覧

    curl -i http://cakephp.test/
    apachectl -M | grep headers
    php -i | grep output_buffering
    

    レスポンスに Content-Length: XXX が含まれていれば、付与は成功しています。


    Content-Length を付けたい理由

    • CDNやロードバランサーでキャッシュ制御をしたい
    • クライアント側で進捗バーを表示したい
    • セキュリティポリシーやWAFでの要件に対応したい

    よくある表示されない理由とその確認方法

    1. レスポンスボディが空またはnull

    本文が何も出力されていないと、Content-Length は出力されません。

    確認方法:

    curl -i http://cakephp.test/
    

    2. ApacheがFastCGIで動作している

    mod_php ではなく FastCGI や php-fpm 経由で動作していると Content-Length が省略されることがあります。

    確認方法:

    apachectl -V | grep -i mpm
    

    または phpinfo()Server API を確認(例:FPM/FastCGI)


    3. Dockerリバースプロキシの影響

    nginxTraefik などがプロキシとして動作していると、ヘッダーが上書きされている可能性があります。

    確認方法:

    docker-compose ps
    

    4. PHPが自動でchunked転送に切り替えている

    バッファが使われていないと Content-Length を使わず Transfer-Encoding: chunked に切り替わることがあります。

    対策例:

    ob_start();
    echo "Content";
    $output = ob_get_clean();
    header('Content-Length: ' . strlen($output));
    echo $output;
    

    5. CakePHPがキャッシュ制御をしている

    CakePHP の withDisabledCache() やミドルウェアでキャッシュ制御が行われていると Content-Length が抑制される場合があります。


    6. HTTP/2使用時にcurlで表示されない

    HTTP/2ではフレーム単位で通信されるため、Content-Length が表示されなくても正常です。

    HTTP/1.1を指定して確認:

    curl -i --http1.1 http://cakephp.test/
    

    7. PHPで出力前にWarningやNoticeが発生している

    ヘッダー送信前にエラー出力があると、Content-Length が送信されないことがあります。


    最終確認:ヘッダー送信のテンプレートスクリプト

    CakePHPを通さずに info.php のようなスクリプトを用意し、最小構成で確認します。

    <?php
    $data = "Test Content";
    header('Content-Length: ' . strlen($data));
    echo $data;
    
    curl -i http://cakephp.test/info.php
    

    チェックリストまとめ

    項目チェック内容
    レスポンスが空でないか本文ありであることを確認
    PHPモードの確認mod_php or php-fpm をチェック
    Proxyによる上書きnginx, Traefik などが動作していないか
    Transfer-Encoding の有無curl --http1.1 で確認
    CakePHPのミドルウェアキャッシュ制御が有効か
    HTTP/2 の仕様curl --http1.1 でフォールバックして確認
    Apacheモジュールmod_headers, mod_deflate などの有無
    PHPバッファ設定output_buffering, zlib.output_compression を確認

    さいごに

    Apacheの設定、PHPの動作モード、CakePHPの挙動などが複雑に絡み合うため、状況によって対策が異なります。
    もし可能であれば 000-default.confDockerfile、CakePHPのコントローラーコードなどを共有していただければ、原因を特定しやすくなります。お気軽にご相談ください。

  • 【検証】CakePHP環境で Content-Length ヘッダーが付与されない理由と確認方法

    CakePHPで開発を進める中、「Content-Length ヘッダーがレスポンスに表示されない」という問題に直面することがあります。通常、このヘッダーは自動で付与されることが多いですが、特定の環境や設定によって表示されなくなるケースも存在します。

    本記事では、Content-Length が通常付与されるケースと、付与されない主な原因について整理し、確認すべき設定とコマンドを紹介します。


    通常 Content-Length が付与される主な原因

    1. PHPからの明示的な出力

    PHP が echoprint で固定長の出力を返すと、自動的に Content-Length が追加されることがあります。

    2. 出力バッファリング(Output Buffering)

    php.inioutput_buffering が有効 (On) になっている場合、PHPは出力全体の長さを事前に計算し、Content-Length を付与します。

    3. Apacheのデフォルト動作

    Apacheは、静的ファイルや一部の動的出力に対して、自動的に Content-Length を付けることがあります。

    4. mod_deflate / mod_gzip 使用時

    圧縮が有効な場合、代わりに Transfer-Encoding: chunked が使用され、Content-Length が省略されることがあります。


    Content-Length が表示されない原因とそのチェック方法

    CakePHPなどのアプリケーション開発時に Content-Length が出力されない場合、以下のいずれかが原因である可能性があります。

    1. Apache設定で Header unset Content-Length を記述している

    000-default.conf.htaccess に次のような記述があると、Apacheがこのヘッダーを消すようになります。

    Header unset Content-Length
    

    2. Transfer-Encoding: chunked が優先されている

    Apacheがチャンク転送(分割レスポンス)を使用していると、Content-Length は明示されません。

    確認コマンド:

    curl -i http://cakephp.test/
    

    ヘッダーに Transfer-Encoding: chunked が含まれているか確認します。

    3. PHPコードで header_remove() が使われている

    たとえば AppController に以下のような記述があると、PHP側で Content-Length を削除しています。

    header_remove('Content-Length');
    

    4. 出力バッファリングが無効

    php.inioutput_buffering = Off に設定されていると、出力の長さが測れず、Content-Length が付与されない場合があります。

    確認コマンド:

    php -i | grep output_buffering
    

    5. Apacheモジュールが無効

    mod_deflatemod_php など、必要なApacheモジュールが無効になっていると、圧縮や出力処理が正しく行われません。

    確認コマンド:

    apachectl -M
    

    状況確認に使えるコマンド一覧

    コマンド説明
    curl -i http://cakephp.test/レスポンスヘッダーを確認
    `apachectl -Mgrep headers`
    `php -igrep output_buffering`

    表形式でのまとめ

    表示されない理由候補チェック方法・補足
    Header unset Content-Length 記述ありApache設定を確認
    Transfer-Encoding: chunked が使用されているcurl -i で確認
    PHPで header_remove() が使われているコントローラやミドルウェアを確認
    output_buffering = Offphp.ini の設定を確認
    Apacheモジュールが無効apachectl -M で確認

    もし問題の原因を特定したい場合は…

    お使いのApache設定ファイル(例: /etc/apache2/sites-enabled/000-default.conf)や、CakePHPのコントローラー設定をご共有いただければ、さらに詳しく調査・解説が可能です。

    開発環境によっては、複数の要因が重なって Content-Length が出力されなくなっていることもあります。1つずつチェックしてみてください。


  • CakePHPでrender()関数を使用してテンプレートに値を渡す方法

    CakePHPでは、テンプレートに値を渡す際にset()メソッドを使用します。本記事では、set()の基本的な使い方や、compact()を利用した変数の一括設定について解説します。


    1. set()を使ってテンプレートに値を渡す

    CakePHPのset()メソッドを使うと、コントローラーで定義した変数をビュー(テンプレート)に渡すことができます。

    コントローラーの記述

    phpコピーする編集するclass ExampleController extends AppController
    {
        public function index()
        {
            // 変数をテンプレートに渡す
            $this->set('message', 'こんにちは、CakePHP!');
            
            // 配列も渡せる
            $this->set('data', [
                'title' => 'CakePHPのテンプレート',
                'description' => 'CakePHPのrenderで値を渡す方法'
            ]);
    
            // 指定したテンプレートをレンダリング
            $this->render('custom_template'); 
        }
    }
    

    このコードでは、set('message', 'こんにちは、CakePHP!')のように値を設定し、ビュー側で使用できるようにしています。


    2. ビュー(テンプレート)側の記述

    set()で渡した変数は、指定したテンプレートファイル内で使用できます。

    テンプレート(templates/Example/custom_template.php

    phpコピーする編集する<h1><?= h($message) ?></h1>
    
    <p>タイトル: <?= h($data['title']) ?></p>
    <p>説明: <?= h($data['description']) ?></p>
    

    このように、コントローラーで設定した変数をテンプレート側で展開できます。


    3. compact()を使って一括で渡す

    compact()関数を使うと、複数の変数を配列として一括で渡すことができます。

    コントローラーの記述

    phpコピーする編集するpublic function index()
    {
        $message = 'こんにちは、CakePHP!';
        $title = 'CakePHPのテンプレート';
        $description = 'CakePHPのrenderで値を渡す方法';
    
        // compact() を使うと複数の変数をまとめて渡せる
        $this->set(compact('message', 'title', 'description'));
    
        // テンプレートを指定
        $this->render('custom_template'); 
    }
    

    この方法を使うと、個別にset()を呼び出す手間が省け、可読性が向上します。


    4. render()の使い方

    CakePHPのrender()メソッドは、指定したテンプレートを表示する役割を持ちます。

    render()の動作

    記述方法動作
    $this->render();アクション名と同じテンプレートを自動で使用(例: index()ならtemplates/Example/index.php
    $this->render('custom_template');指定したテンプレート(例: templates/Example/custom_template.php)を使用

    デフォルトでは、render()を明示的に指定しなくても、アクション名と同じテンプレートが表示されます。しかし、カスタムのテンプレートを指定したい場合は$this->render('custom_template');のように記述します。


    5. まとめ

    方法書き方特徴
    単一の変数を渡す$this->set('変数名', 値);変数を1つずつ渡せる
    配列を渡す$this->set('変数名', ['key' => 'value']);配列をビューに渡せる
    compact() を使う$this->set(compact('var1', 'var2'));複数の変数をまとめて渡せる

    このように、CakePHPではset()を活用して、コントローラーからビューへ簡単にデータを渡すことができます。compact()を使うことで、可読性を向上させつつ、スムーズにデータを管理できます。

    CakePHPのテンプレートを柔軟に扱えるようになり、より効率的な開発が可能になります。

  • APIにおけるタイムアウト判定の実装と方法

    APIのサーバー側でタイムアウトを適切に判定するには、API全体のタイムアウト管理と個別の処理(外部API・内部処理)のタイムアウト管理を適切に設計する必要があります。本記事では、タイムアウト判定を行う場所と方法について詳しく解説します。


    1. タイムアウト判定を行う場所

    ① API全体のタイムアウト管理

    判定するタイミング:

    • 処理の開始時に 開始時間を記録$startTime = microtime(true);
    • 各処理の前に 残り時間を計算getRemainingTime() で確認)
    • 残り時間が 0 以下 になったら 即エラーを返す

    ② 外部API呼び出し時(リトライあり)

    判定するタイミング:

    • 外部APIのレスポンスが 指定時間を超えた場合
    • curl_setopt($ch, CURLOPT_TIMEOUT, min(getRemainingTime(), 5)); を設定し、API全体のタイムアウトを考慮
    • リトライ時に getRemainingTime() を確認
    phpコピーする編集するif (getRemainingTime() <= 0) { 
        throw new TimeoutException("External API timeout.", "external_api"); 
    }
    

    ③ 内部処理(データベース処理やロジック処理)(リトライなし)

    判定するタイミング:

    • 処理開始前に残り時間を確認
    phpコピーする編集するif (getRemainingTime() <= 0) { 
        throw new TimeoutException("Internal processing timeout.", "internal_process"); 
    }
    
    • データベース接続時に PDO::ATTR_TIMEOUT を設定
    phpコピーする編集するPDO::ATTR_TIMEOUT => min(getRemainingTime(), 5);
    

    2. タイムアウト判定の実装

    以下のコードでは、各処理の前に getRemainingTime() を呼び出し、残り時間が 0 以下なら即エラーを返します。

    API全体の処理

    phpコピーする編集するheader("Content-Type: application/json");
    
    define("TOTAL_TIMEOUT", 10); // API全体のタイムアウト(秒)
    define("EXTERNAL_API_MAX_RETRIES", 3); // 外部APIの最大リトライ回数
    
    $startTime = microtime(true); // 開始時間
    
    set_time_limit(TOTAL_TIMEOUT); // PHPスクリプトの最大実行時間を設定
    
    try {
        // 外部APIの呼び出し(リトライあり & タイムアウト監視)
        $externalData = callExternalApiWithRetry("https://api.example.com/data", EXTERNAL_API_MAX_RETRIES);
    
        // 内部処理の実行(リトライなし & タイムアウト監視)
        $internalData = executeInternalProcessing();
    
        // 全体のレスポンスを返す
        response(200, ["external" => $externalData, "internal" => $internalData]);
    
    } catch (TimeoutException $e) {
        response(504, [
            "status" => "error",
            "message" => "Gateway Timeout: " . $e->getMessage(),
            "error_source" => $e->getSource()
        ]);
    
    } catch (Exception $e) {
        response(500, [
            "status" => "error",
            "message" => "Internal Server Error: " . $e->getMessage(),
            "error_source" => "unknown"
        ]);
    }
    

    外部API呼び出し時(リトライあり & タイムアウト監視)

    phpコピーする編集するfunction callExternalApiWithRetry($url, $maxRetries)
    {
        global $startTime;
        $attempt = 0;
    
        while ($attempt < $maxRetries) {
            $attempt++;
            try {
                // API全体の残り時間をチェック
                if (getRemainingTime() <= 0) {
                    throw new TimeoutException("API request exceeded total timeout.", "external_api");
                }
    
                $ch = curl_init($url);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_TIMEOUT, min(getRemainingTime(), 5)); // 外部APIのタイムアウト
                curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
                $response = curl_exec($ch);
                $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                curl_close($ch);
    
                if ($httpCode === 200 && $response) {
                    return json_decode($response, true);
                }
    
                throw new Exception("HTTP $httpCode - API response error");
    
            } catch (Exception $e) {
                if ($attempt >= $maxRetries) {
                    throw new TimeoutException("External API failed after retries.", "external_api");
                }
                sleep(1); // 1秒待機後に再試行
            }
        }
    }
    

    内部処理(リトライなし & タイムアウト監視)

    phpコピーする編集するfunction executeInternalProcessing()
    {
        // API全体の残り時間をチェック
        if (getRemainingTime() <= 0) {
            throw new TimeoutException("Internal processing exceeded total timeout.", "internal_process");
        }
    
        try {
            $pdo = new PDO("mysql:host=localhost;dbname=testdb", "user", "password", [
                PDO::ATTR_TIMEOUT => min(getRemainingTime(), 5),
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
            ]);
    
            $stmt = $pdo->prepare("SELECT * FROM users WHERE active = 1");
            $stmt->execute();
    
            return $stmt->fetchAll(PDO::FETCH_ASSOC);
    
        } catch (PDOException $e) {
            throw new TimeoutException("Database Error: " . $e->getMessage(), "internal_process");
        }
    }
    

    タイムアウト計測

    phpコピーする編集するfunction getRemainingTime()
    {
        global $startTime;
        return max(0, TOTAL_TIMEOUT - (microtime(true) - $startTime));
    }
    

    タイムアウト発生時のエラーハンドリング

    phpコピーする編集するclass TimeoutException extends Exception
    {
        private $source;
    
        public function __construct($message, $source)
        {
            parent::__construct($message);
            $this->source = $source;
        }
    
        public function getSource()
        {
            return $this->source;
        }
    }
    

    3. タイムアウト判定のまとめ

    判定する処理どこで判定する?どのように判定する?エラーメッセージ
    API全体のタイムアウト各処理の開始前getRemainingTime() <= 0"API request exceeded total timeout."
    外部APIのレスポンスが遅いcURLオプションCURLOPT_TIMEOUT = min(getRemainingTime(), 5)"External API failed after retries."
    内部処理(DB処理含む)が遅い処理開始前 & DB接続PDO::ATTR_TIMEOUT = min(getRemainingTime(), 5)"Internal processing exceeded total timeout."

    4. まとめ

    • API全体のタイムアウト (TOTAL_TIMEOUT) を統一し、各処理がAPI全体のタイムアウトを超えないようにする
    • getRemainingTime() を使い、各処理の前に残り時間を確認し、超えていたら即エラーを返す
    • 外部APIのタイムアウトは CURLOPT_TIMEOUT で設定し、最大リトライ回数 (EXTERNAL_API_MAX_RETRIES) を制御
    • 内部処理は PDO::ATTR_TIMEOUT を使い、リトライなしで処理
    • タイムアウト発生時のエラーメッセージに error_source を追加し、どこで発生したかを明確にする

    この実装により、クライアント側から見ても統一されたレスポンスになりつつ、内部でのタイムアウト発生場所も特定できるようになります。

  • 外部処理と内部処理を合わせたタイムアウトの判定

    APIの処理において、外部サービスとの通信や内部処理の実行が遅延した場合に、どこでタイムアウトが発生したのかを明確に判別できる仕組みを構築することが重要です。本記事では、外部APIのリトライありタイムアウト、内部処理のリトライなしタイムアウトを統一的に管理し、エラーの発生箇所を特定できる方法を紹介します。


    1. 設計方針

    API全体のタイムアウトを統一し、外部APIアクセスと内部処理の制御を行います。

    基本方針

    1. API全体のタイムアウトを統合
      • TOTAL_TIMEOUT = 10秒 で、外部APIと内部処理を含めた全体のタイムアウトを設定。
    2. 外部APIはリトライあり
      • EXTERNAL_API_MAX_RETRIES = 3(最大3回リトライ)。
      • 1回のAPI呼び出しのタイムアウトは min(残り時間, 5秒) で設定。
    3. 内部処理はリトライなし
      • PDO::ATTR_TIMEOUT を設定し、リクエストが全体の残り時間を超えないようにする。
    4. タイムアウト発生時に発生箇所を明確化
      • 外部APIでタイムアウトerror_source: "external_api"
      • 内部処理でタイムアウトerror_source: "internal_process"
      • API全体でタイムアウトerror_source: "api_timeout"

    2. タイムアウトを統合したPHPコード

    以下のコードでは、外部APIと内部処理の両方にタイムアウト制御を導入し、どこで問題が発生したのかを特定できるようにしています。

    phpコピーする編集するheader("Content-Type: application/json");
    
    define("TOTAL_TIMEOUT", 10); // API全体のタイムアウト(秒)
    define("EXTERNAL_API_MAX_RETRIES", 3); // 外部APIの最大リトライ回数
    
    $startTime = microtime(true); // APIの開始時間
    set_time_limit(TOTAL_TIMEOUT); // PHPスクリプトの最大実行時間を設定
    
    try {
        // 外部APIの呼び出し(リトライあり & タイムアウト監視)
        $externalData = callExternalApiWithRetry("https://api.example.com/data", EXTERNAL_API_MAX_RETRIES);
    
        // 内部処理の実行(リトライなし & タイムアウト監視)
        $internalData = executeInternalProcessing();
    
        // 全体のレスポンスを返す
        response(200, ["external" => $externalData, "internal" => $internalData]);
    
    } catch (TimeoutException $e) {
        response(504, [
            "status" => "error",
            "message" => "Gateway Timeout: " . $e->getMessage(),
            "error_source" => $e->getSource()
        ]);
    
    } catch (Exception $e) {
        response(500, [
            "status" => "error",
            "message" => "Internal Server Error: " . $e->getMessage(),
            "error_source" => "unknown"
        ]);
    }
    
    /**
     * 外部サービスの呼び出し(リトライあり & 残り時間考慮)
     */
    function callExternalApiWithRetry($url, $maxRetries)
    {
        global $startTime;
        $attempt = 0;
    
        while ($attempt < $maxRetries) {
            $attempt++;
            try {
                if (getRemainingTime() <= 0) {
                    throw new TimeoutException("API request exceeded total timeout.", "external_api");
                }
    
                $ch = curl_init($url);
                curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
                curl_setopt($ch, CURLOPT_TIMEOUT, min(getRemainingTime(), 5)); // 外部APIのタイムアウト
                curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
                $response = curl_exec($ch);
                $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
                curl_close($ch);
    
                if ($httpCode === 200 && $response) {
                    return json_decode($response, true);
                }
    
                throw new Exception("HTTP $httpCode - API response error");
    
            } catch (Exception $e) {
                if ($attempt >= $maxRetries) {
                    throw new TimeoutException("External API failed after retries.", "external_api");
                }
                sleep(1); // 1秒待機後に再試行
            }
        }
    }
    
    /**
     * 内部処理(リトライなし & 残り時間考慮)
     */
    function executeInternalProcessing()
    {
        if (getRemainingTime() <= 0) {
            throw new TimeoutException("Internal processing exceeded total timeout.", "internal_process");
        }
    
        try {
            $pdo = new PDO("mysql:host=localhost;dbname=testdb", "user", "password", [
                PDO::ATTR_TIMEOUT => min(getRemainingTime(), 5),
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
            ]);
    
            $stmt = $pdo->prepare("SELECT * FROM users WHERE active = 1");
            $stmt->execute();
    
            return $stmt->fetchAll(PDO::FETCH_ASSOC);
    
        } catch (PDOException $e) {
            throw new TimeoutException("Database Error: " . $e->getMessage(), "internal_process");
        }
    }
    
    /**
     * 残り時間を計算(API全体のタイムアウトを超えないように)
     */
    function getRemainingTime()
    {
        global $startTime;
        return max(0, TOTAL_TIMEOUT - (microtime(true) - $startTime));
    }
    
    /**
     * 正常なレスポンスを返す
     */
    function response($statusCode, $data)
    {
        http_response_code($statusCode);
        echo json_encode($data);
        exit;
    }
    
    /**
     * タイムアウト用のカスタム例外クラス
     */
    class TimeoutException extends Exception
    {
        private $source;
    
        public function __construct($message, $source)
        {
            parent::__construct($message);
            $this->source = $source;
        }
    
        public function getSource()
        {
            return $this->source;
        }
    }
    

    3. 仕組みのポイント

    1. API全体のタイムアウト管理
      • set_time_limit(TOTAL_TIMEOUT); でスクリプト全体の最大実行時間を設定。
      • getRemainingTime() を使用し、外部APIと内部処理の両方がAPI全体のタイムアウトを超えないよう制御。
    2. タイムアウトの発生場所を特定
      • 外部APIでタイムアウト → error_source: "external_api"
      • 内部処理でタイムアウト → error_source: "internal_process"
      • API全体のタイムアウト → error_source: "api_timeout"
    3. 外部APIのリトライ時にもAPI全体の残り時間を考慮
      • curl_setopt($ch, CURLOPT_TIMEOUT, min(getRemainingTime(), 5));
      • 最大3回リトライし、それでも失敗したら 504 Gateway Timeout を返す。
    4. 内部処理はリトライなし & タイムアウト即終了
      • PDO::ATTR_TIMEOUT => min(getRemainingTime(), 5);
      • getRemainingTime() をチェックし、時間切れなら即 504 Gateway Timeout を返す。

    4. まとめ

    • API全体のタイムアウト管理を統一し、外部API・内部処理を統合。
    • タイムアウト発生時に、外部APIが原因か内部処理が原因かを明確に判別可能。
    • エラーレスポンスに error_source を追加し、クライアント側が適切な対応を取れるようにする。

    この方法を適用することで、APIの安定性を高め、エラー発生時のトラブルシューティングを容易にすることができます。

  • API通信でサーバー側が400系・500系エラーを判定・検証するPHPコード

    APIを提供するサーバー側では、リクエストのバリデーションや内部処理の結果に応じて、適切なHTTPステータスコードを返す必要があります。本記事では、PHPを使用したAPIサーバーの構築例と、400系(クライアントエラー)や500系(サーバーエラー)の判定方法について解説します。


    1. 基本的なAPIサーバーの構成

    PHPでAPIを作成し、400系・500系のエラーを適切に判定・返すコードの例を紹介します。

    基本のAPIコード

    phpコピーする編集するheader("Content-Type: application/json");
    
    // リクエストメソッドを取得
    $method = $_SERVER['REQUEST_METHOD'];
    
    try {
        if ($method === "GET") {
            handleGetRequest();
        } elseif ($method === "POST") {
            handlePostRequest();
        } else {
            throwError(405, "Method Not Allowed");
        }
    } catch (Exception $e) {
        throwError(500, "Internal Server Error: " . $e->getMessage());
    }
    
    /**
     * GETリクエストの処理
     */
    function handleGetRequest()
    {
        $users = [
            ["id" => 1, "name" => "John Doe"],
            ["id" => 2, "name" => "Jane Doe"]
        ];
    
        response(200, ["status" => "success", "data" => $users]);
    }
    
    /**
     * POSTリクエストの処理(バリデーションあり)
     */
    function handlePostRequest()
    {
        $input = json_decode(file_get_contents("php://input"), true);
    
        if (!isset($input['name']) || empty($input['name'])) {
            throwError(400, "Invalid request. 'name' is required.");
        }
    
        response(201, ["status" => "success", "message" => "User created"]);
    }
    
    /**
     * 正常なレスポンスを返す
     */
    function response($statusCode, $data)
    {
        http_response_code($statusCode);
        echo json_encode($data);
        exit;
    }
    
    /**
     * エラー時のレスポンスを統一
     */
    function throwError($statusCode, $message)
    {
        http_response_code($statusCode);
        echo json_encode(["status" => "error", "message" => $message]);
        exit;
    }
    

    2. 各エラーハンドリングの詳細

    400 Bad Request

    phpコピーする編集するif (!isset($input['name']) || empty($input['name'])) {
        throwError(400, "Invalid request. 'name' is required.");
    }
    

    主な原因

    • JSONフォーマットが間違っている
    • 必須パラメータが不足している

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "Invalid request. 'name' is required."
    }
    

    401 Unauthorized

    phpコピーする編集するif (!isset($_SERVER['HTTP_AUTHORIZATION']) || $_SERVER['HTTP_AUTHORIZATION'] !== 'Bearer valid_token') {
        throwError(401, "Unauthorized: Invalid token.");
    }
    

    主な原因

    • 認証トークンがない
    • 認証トークンが不正

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "Unauthorized: Invalid token."
    }
    

    403 Forbidden

    phpコピーする編集するif ($userRole !== "admin") {
        throwError(403, "Forbidden: You do not have permission.");
    }
    

    主な原因

    • アクセス権限が不足している

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "Forbidden: You do not have permission."
    }
    

    404 Not Found

    phpコピーする編集するif ($userId !== 1) {
        throwError(404, "User not found.");
    }
    

    主な原因

    • 指定されたリソースが存在しない

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "User not found."
    }
    

    405 Method Not Allowed

    phpコピーする編集するif ($method !== "GET" && $method !== "POST") {
        throwError(405, "Method Not Allowed");
    }
    

    主な原因

    • GET のみ許可されているAPIに POST でリクエストしている

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "Method Not Allowed"
    }
    

    3. 500系(サーバーエラー)の処理

    500 Internal Server Error

    phpコピーする編集するtry {
        throw new Exception("Something went wrong");
    } catch (Exception $e) {
        throwError(500, "Internal Server Error: " . $e->getMessage());
    }
    

    主な原因

    • プログラムのバグ
    • データベース接続エラー
    • メモリ不足

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "Internal Server Error: Something went wrong"
    }
    

    502 Bad Gateway

    phpコピーする編集するif (!$backendApiResponse) {
        throwError(502, "Bad Gateway: Backend API not responding.");
    }
    

    主な原因

    • リバースプロキシ(Nginx, AWS ALBなど)がバックエンドと通信できない

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "Bad Gateway: Backend API not responding."
    }
    

    503 Service Unavailable

    phpコピーする編集するif ($serverLoad > 90) {
        throwError(503, "Service Unavailable: Server is overloaded.");
    }
    

    主な原因

    • サーバーが過負荷状態

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "Service Unavailable: Server is overloaded."
    }
    

    504 Gateway Timeout

    phpコピーする編集するif ($apiResponseTime > 30) {
        throwError(504, "Gateway Timeout: API response too slow.");
    }
    

    主な原因

    • バックエンドAPIのレスポンスが遅すぎる

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "Gateway Timeout: API response too slow."
    }
    

    4. まとめ

    ステータスコードエラー内容主な原因
    400Bad RequestJSONフォーマットエラー、必須パラメータ不足
    401UnauthorizedAPIキー・JWTトークンが無効
    403Forbiddenアクセス権限がない
    404Not FoundURL間違い、リソースが存在しない
    405Method Not AllowedHTTPメソッド間違い
    500Internal Server Errorサーバーのバグ、DBエラー
    502Bad Gatewayリバースプロキシがバックエンドに接続不可
    503Service Unavailableサーバー過負荷
    504Gateway TimeoutAPI応答が遅すぎる

    PHPで適切に400系・500系のエラーハンドリングを行うことで、APIの信頼性を向上させることができます。
    エラーハンドリングの実装を適切に行い、スムーズなAPI運用を目指しましょう。

  • API通信で発生する400系・500系エラーの原因と対策

    API通信において 400系(クライアントエラー)500系(サーバーエラー) が発生する場合、それぞれの原因を理解し、適切に対処することが重要です。
    本記事では、各エラーコードの意味・主な原因・解決策を解説します。


    1. 400系(クライアントエラー)

    クライアント側のリクエストに誤りがある場合に発生するエラーです。

    400 Bad Request

    意味: リクエストが無効(フォーマットミスや不正なデータ)

    主な原因

    • JSONフォーマットの誤り
    • 必須パラメータが不足
    • データ型が不正(例: int なのに string を送信)
    • エンコードエラー(文字化け)
    • リクエストボディが空
    • APIのバージョン違い

    解決策

    • リクエストデータを API仕様書と照合 する
    • JSONのフォーマットを 検証ツール(JSONLintなど) でチェック
    • APIのバージョンを確認する

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "Invalid request format",
        "details": {
            "field": "email",
            "error": "Email is required"
        }
    }
    

    401 Unauthorized

    意味: 認証に失敗(APIキーやトークンが無効)

    主な原因

    • 認証情報(APIキー、JWTトークン)が間違っている
    • トークンの有効期限が切れている
    • 認証ヘッダーがない
    • OAuthの認証フローに問題がある

    解決策

    • APIキーやトークンを再発行
    • ヘッダーに正しく Authorization を設定
    • JWTの有効期限を確認

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "Invalid token"
    }
    

    403 Forbidden

    意味: 認証は成功したが、アクセス権限がない

    主な原因

    • APIキーにアクセス権がない
    • CORS(クロスオリジン制約)によるブロック
    • IP制限
    • 特定のリソースへのアクセス禁止(例: 管理者専用)

    解決策

    • APIの アクセス権限(RBAC) を確認
    • CORS設定を確認
    • IP制限がある場合は許可リストを確認

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "You do not have permission to access this resource"
    }
    

    404 Not Found

    意味: リソースが見つからない(URL間違い)

    主な原因

    • エンドポイントが存在しない
    • URLのパスパラメータが間違っている
    • リソースが削除されている

    解決策

    • API仕様書 と照らし合わせて URLを確認
    • /users/{id} などの IDが有効か確認
    • デバッグログで APIがどのURLを呼び出しているか確認

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "User not found"
    }
    

    405 Method Not Allowed

    意味: 対応していないHTTPメソッドを使用

    主な原因

    • GET しか対応していないAPIに POST で送信
    • CORSの設定ミス

    解決策

    • APIが対応する HTTPメソッドを確認
    • クライアント側のリクエスト設定を修正

    2. 500系(サーバーエラー)

    サーバー側の問題により、リクエストが処理できない場合に発生するエラーです。

    500 Internal Server Error

    意味: サーバー内部エラー(一般的なエラー)

    主な原因

    • プログラムのバグ
    • データベース接続エラー
    • Nullポインタ例外(未定義の変数を参照)
    • メモリ不足
    • ログ出力時のエラー

    解決策

    • サーバーログを確認
    • DB接続設定をチェック
    • 例外処理 (try-catch) を強化

    エラーレスポンス例

    jsonコピーする編集する{
        "status": "error",
        "message": "An unexpected error occurred"
    }
    

    502 Bad Gateway

    意味: リバースプロキシ(Nginx, AWS ALB など)がバックエンドと通信できない

    主な原因

    • バックエンドAPIが落ちている
    • サーバー間のネットワークエラー
    • ロードバランサーの設定ミス

    解決策

    • バックエンドサーバーが 動作しているか確認
    • Nginxやロードバランサーの設定を確認
    • curl コマンドでAPIが生きているかテスト

    テストコマンド

    bashコピーする編集するcurl -I http://backend-server/api/health
    

    503 Service Unavailable

    意味: サーバーが一時的に利用不可

    主な原因

    • メンテナンス中
    • 過負荷(リクエストが多すぎる)
    • データベースの接続上限超過

    解決策

    • サーバーの負荷を確認 (top, htop, vmstat)
    • スケールアウト(負荷分散)を検討
    • レートリミット(制限)を実装

    504 Gateway Timeout

    意味: バックエンドAPIの応答が遅い

    主な原因

    • DBクエリの処理時間が長すぎる
    • サーバーのレスポンスが遅い
    • ロードバランサーのタイムアウト設定

    解決策

    • クエリの最適化 (EXPLAIN ANALYZE で遅いSQLを特定)
    • APIのタイムアウト設定を調整
    • 負荷分散(スケールアップ/スケールアウト)を検討

    3. まとめ

    エラーコード原因
    400JSONフォーマットミス、必須パラメータ不足
    401認証エラー(APIキー、トークン不正)
    403アクセス権限がない
    404URL間違い、リソースが存在しない
    405HTTPメソッド間違い
    500サーバー内部エラー、コードバグ
    502リバースプロキシがバックエンドに接続できない
    503サーバー過負荷、メンテナンス中
    504APIの処理が遅すぎる(タイムアウト)

    APIのエラーハンドリングを適切に行うことで、信頼性の高いシステムを構築できます。適切なエラーメッセージと対策を実装し、スムーズなAPI運用を目指しましょう。