Python requests での様々な HTTP ステータスコードの処理方法

PythonBeginner
オンラインで実践に進む

はじめに

このチュートリアルでは、Python の requests ライブラリで様々な HTTP ステータスコードを扱う方法を説明します。HTTP ステータスコードは、Web リクエストが成功したか失敗したかを理解し、様々なシナリオに適切に対応するために不可欠です。この実験(Lab)の終わりには、Web サービスと連携する Python アプリケーションで、堅牢なエラーハンドリングを実装する方法を習得できます。

まず、HTTP ステータスコードの理解から始め、基本的なエラーハンドリングと高度なエラーハンドリング技術を段階的に習得していきます。この知識は、様々なサーバーからのレスポンスを適切に処理する、信頼性の高い Python Web アプリケーションを開発するために不可欠です。

HTTP ステータスコードの理解とセットアップ

HTTP ステータスコードとは?

HTTP ステータスコードは、クライアントのリクエストに対する応答としてサーバーから返される 3 桁の数字です。これらは、特定の HTTP リクエストが正常に完了したかどうかを示します。これらのコードは、次の 5 つのカテゴリに分類されます。

  • 1xx (Informational): リクエストは受信され、処理が継続中です
  • 2xx (Success): リクエストは正常に受信、理解、および受け入れられました
  • 3xx (Redirection): リクエストを完了するために、さらなるアクションが必要です
  • 4xx (Client Error): リクエストには誤った構文が含まれているか、実行できません
  • 5xx (Server Error): サーバーが、一見有効なリクエストの実行に失敗しました

よく遭遇するステータスコードには、以下のようなものがあります。

ステータスコード 名前 説明
200 OK リクエストは成功しました
201 Created リクエストは成功し、リソースが作成されました
400 Bad Request サーバーはリクエストを処理できません
401 Unauthorized 認証が必要です
404 Not Found 要求されたリソースが見つかりませんでした
500 Internal Server Error サーバーで予期しない状態が発生しました

Python 環境のセットアップ

HTTP ステータスコードの処理を開始する前に、Python の requests ライブラリをインストールし、簡単なテストスクリプトを作成しましょう。

  1. 実験(LabEx)環境でターミナルを開き、以下を実行します。
pip install requests
  1. プロジェクトファイル用の新しいディレクトリを作成します。
mkdir -p ~/project/http_status_lab
cd ~/project/http_status_lab
  1. WebIDE を使用して、http_status_lab ディレクトリに test_request.py という名前の新しいファイルを作成し、次の基本的なコードを記述します。
import requests

def make_request(url):
    """Make a basic GET request to the specified URL."""
    response = requests.get(url)
    print(f"Status Code: {response.status_code}")
    print(f"Response: {response.text[:100]}...")  ## Print first 100 characters
    return response

## Test with a working URL
response = make_request("https://httpbin.org/status/200")
print(f"Request was successful: {response.ok}")
  1. テストスクリプトを実行して、動作を確認します。
python test_request.py

次のような出力が表示されるはずです。

Status Code: 200
Response: ...
Request was successful: True

これにより、Python 環境が正常にセットアップされ、基本的な HTTP リクエストを実行できることが確認されました。次のステップでは、様々な HTTP ステータスコードを処理し、より高度なエラーハンドリング技術を実装する方法を学びます。

基本的な HTTP ステータスコードの処理

HTTP ステータスコードが何かを理解し、環境をセットアップしたので、一般的なステータスコードに対する基本的なエラーハンドリングを実装しましょう。

ステータスコードテストツールの作成

まず、様々な HTTP ステータスコードをテストできるツールを作成しましょう。Web サイト httpbin.org は、特定のステータスコードを返すエンドポイントを提供しており、テストに最適です。

http_status_lab ディレクトリに、次のコードを含む status_code_tester.py という名前の新しいファイルを作成します。

import requests
import sys

def test_status_code(status_code):
    """Test a specific HTTP status code using httpbin.org."""
    url = f"https://httpbin.org/status/{status_code}"
    try:
        response = requests.get(url)
        print(f"Status Code: {response.status_code}")
        print(f"Response OK: {response.ok}")
        return response
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
        return None

if __name__ == "__main__":
    ## Get status code from command line or use default
    status_code = sys.argv[1] if len(sys.argv) > 1 else "200"
    test_status_code(status_code)

このスクリプトは、特定のステータスコードで httpbin.org にリクエストを送信し、その結果を出力します。コマンドライン引数を使用して、様々なステータスコードをテストできます。

様々なステータスコードのテスト

スクリプトを様々なステータスコードでテストし、requests ライブラリがどのように処理するかを確認しましょう。

  1. 成功したリクエスト (200 OK) をテストします。
python status_code_tester.py 200

期待される出力:

Status Code: 200
Response OK: True
  1. クライアントエラー (404 Not Found) をテストします。
python status_code_tester.py 404

期待される出力:

Status Code: 404
Response OK: False
  1. サーバーエラー (500 Internal Server Error) をテストします。
python status_code_tester.py 500

期待される出力:

Status Code: 500
Response OK: False

基本的なステータスコードカテゴリの処理

次に、様々なカテゴリのステータスコードを適切に処理する、より包括的なスクリプトを作成しましょう。http_status_lab ディレクトリに、basic_handler.py という名前の新しいファイルを作成します。

import requests

def handle_response(url):
    """Handle different HTTP status codes with basic error handling."""
    try:
        response = requests.get(url)

        ## Check status code category
        if 200 <= response.status_code < 300:
            print(f"Success! Status code: {response.status_code}")
            return response
        elif 300 <= response.status_code < 400:
            print(f"Redirection! Status code: {response.status_code}")
            ## For redirection, you might want to follow the redirect
            return response
        elif 400 <= response.status_code < 500:
            print(f"Client error! Status code: {response.status_code}")
            ## Handle client errors
            return response
        elif 500 <= response.status_code < 600:
            print(f"Server error! Status code: {response.status_code}")
            ## Handle server errors
            return response
    except requests.exceptions.RequestException as e:
        print(f"Request failed: {e}")
        return None

## Test with different status codes
print("Testing 200 OK:")
handle_response("https://httpbin.org/status/200")

print("\nTesting 404 Not Found:")
handle_response("https://httpbin.org/status/404")

print("\nTesting 500 Internal Server Error:")
handle_response("https://httpbin.org/status/500")

スクリプトを実行して、様々なステータスコードがどのように処理されるかを確認します。

python basic_handler.py

期待される出力:

Testing 200 OK:
Success! Status code: 200

Testing 404 Not Found:
Client error! Status code: 404

Testing 500 Internal Server Error:
Server error! Status code: 500

この基本的なハンドラーは、ステータスコードをカテゴリにグループ化し、カテゴリに基づいて異なるアクションを実行します。これは、Python Web アプリケーションで一般的なパターンであり、様々な種類のレスポンスを適切に処理できます。

高度なエラーハンドリングの実装

HTTP ステータスコードの基本的な処理を理解したので、Python アプリケーション向けに、より高度なエラーハンドリング技術を実装しましょう。

raise_for_status() の使用

requests ライブラリには、4xx および 5xx ステータスコードに対して例外を発生させる便利なメソッド raise_for_status() があります。これは、コード内でエラーを処理するためのシンプルかつ効果的な方法です。

http_status_lab ディレクトリに、raise_for_status_example.py という名前の新しいファイルを作成します。

import requests

def fetch_data(url):
    """Fetch data from a URL with error handling using raise_for_status()."""
    try:
        response = requests.get(url)
        ## This will raise an HTTPError if the HTTP request returned an unsuccessful status code
        response.raise_for_status()
        ## If we get here, the request was successful
        print(f"Success! Status code: {response.status_code}")
        return response.json() if 'application/json' in response.headers.get('Content-Type', '') else response.text
    except requests.exceptions.HTTPError as e:
        print(f"HTTP Error: {e}")
        return None
    except requests.exceptions.ConnectionError as e:
        print(f"Connection Error: {e}")
        return None
    except requests.exceptions.Timeout as e:
        print(f"Timeout Error: {e}")
        return None
    except requests.exceptions.RequestException as e:
        print(f"Request Exception: {e}")
        return None

## Test with different URLs
print("Testing successful request:")
data = fetch_data("https://httpbin.org/json")
print(f"Received data type: {type(data)}")

print("\nTesting 404 error:")
data = fetch_data("https://httpbin.org/status/404")
print(f"Received data: {data}")

print("\nTesting 500 error:")
data = fetch_data("https://httpbin.org/status/500")
print(f"Received data: {data}")

スクリプトを実行して、raise_for_status() がどのようにエラーを処理するかを確認します。

python raise_for_status_example.py

期待される出力は、ライブラリが 2xx 以外のステータスコードに対して自動的に例外を発生させ、コードがそれをキャッチして適切に処理できることを示します。

カスタム例外ハンドラーの作成

より複雑なアプリケーションでは、より具体的なエラーハンドリングを提供するために、カスタム例外ハンドラーを作成することがあります。より高度な例を作成しましょう。

http_status_lab ディレクトリに、custom_exception_handler.py という名前の新しいファイルを作成します。

import requests
import logging

## Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

## Custom exceptions
class APIError(Exception):
    """Base class for API request exceptions."""
    pass

class ClientError(APIError):
    """Exception raised for client errors (4xx)."""
    pass

class ServerError(APIError):
    """Exception raised for server errors (5xx)."""
    pass

class NotFoundError(ClientError):
    """Exception raised for 404 Not Found errors."""
    pass

class UnauthorizedError(ClientError):
    """Exception raised for 401 Unauthorized errors."""
    pass

def api_request(url, method="get", **kwargs):
    """Make an API request with comprehensive error handling."""
    try:
        ## Make the request using the specified HTTP method
        response = getattr(requests, method.lower())(url, **kwargs)

        ## Log the response status
        logger.info(f"Request to {url} returned status code {response.status_code}")

        ## Handle different status codes
        if 200 <= response.status_code < 300:
            return response
        elif response.status_code == 401:
            raise UnauthorizedError(f"Authentication required: {response.text}")
        elif response.status_code == 404:
            raise NotFoundError(f"Resource not found: {url}")
        elif 400 <= response.status_code < 500:
            raise ClientError(f"Client error {response.status_code}: {response.text}")
        elif 500 <= response.status_code < 600:
            raise ServerError(f"Server error {response.status_code}: {response.text}")
        else:
            raise APIError(f"Unexpected status code {response.status_code}: {response.text}")

    except requests.exceptions.RequestException as e:
        logger.error(f"Request failed: {e}")
        raise APIError(f"Request failed: {e}")

## Test the custom exception handler
def test_api_request(url):
    """Test the api_request function with error handling."""
    try:
        response = api_request(url)
        print(f"Success! Status code: {response.status_code}")
        print(f"Response: {response.text[:100]}...")  ## Print first 100 characters
        return response
    except UnauthorizedError as e:
        print(f"Authentication Error: {e}")
    except NotFoundError as e:
        print(f"Not Found Error: {e}")
    except ClientError as e:
        print(f"Client Error: {e}")
    except ServerError as e:
        print(f"Server Error: {e}")
    except APIError as e:
        print(f"API Error: {e}")
    return None

## Test with different status codes
print("Testing 200 OK:")
test_api_request("https://httpbin.org/status/200")

print("\nTesting 404 Not Found:")
test_api_request("https://httpbin.org/status/404")

print("\nTesting 401 Unauthorized:")
test_api_request("https://httpbin.org/status/401")

print("\nTesting 500 Internal Server Error:")
test_api_request("https://httpbin.org/status/500")

スクリプトを実行して、カスタム例外ハンドリングがどのように機能するかを確認します。

python custom_exception_handler.py

この高度な例は、いくつかの重要な概念を示しています。

  1. 基本的な Exception クラスを拡張するカスタム例外クラス
  2. 様々な種類のエラーに対する階層的な例外
  3. デバッグと監視のための詳細なロギング
  4. 様々なステータスコードに対する具体的な処理

このアプローチにより、様々なエラータイプをより構造化され、保守性の高い方法で処理できるようになり、大規模なアプリケーションには不可欠です。

完全な Web API クライアントの構築

この最終ステップでは、学習したすべてのことを組み合わせて、堅牢なエラーハンドリングを備えた完全な Web API クライアントを構築します。テストに使用できる無料のオンライン REST API である JSONPlaceholder API のクライアントを作成します。

簡単な API クライアントの作成

http_status_lab ディレクトリに、api_client.py という名前のファイルを作成しましょう。

import requests
import json
import logging

## Configure logging
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

class APIClient:
    """A simple API client with robust error handling."""

    def __init__(self, base_url):
        """Initialize the API client with a base URL."""
        self.base_url = base_url
        self.session = requests.Session()

    def request(self, endpoint, method="get", params=None, data=None, headers=None):
        """Make a request to the API with error handling."""
        url = f"{self.base_url}{endpoint}"

        ## Default headers for JSON APIs
        if headers is None:
            headers = {
                'Content-Type': 'application/json',
                'Accept': 'application/json'
            }

        ## Convert data to JSON if it's a dictionary
        json_data = None
        if data and isinstance(data, dict):
            json_data = data
            data = None

        try:
            ## Log the request
            logger.info(f"Making {method.upper()} request to {url}")

            ## Make the request
            response = self.session.request(
                method=method,
                url=url,
                params=params,
                data=data,
                json=json_data,
                headers=headers
            )

            ## Log the response status
            logger.info(f"Received response with status code {response.status_code}")

            ## Check for HTTP errors
            response.raise_for_status()

            ## Parse JSON response if applicable
            try:
                return response.json()
            except json.JSONDecodeError:
                return response.text

        except requests.exceptions.HTTPError as e:
            status_code = e.response.status_code
            error_message = f"HTTP Error: {status_code}"

            try:
                ## Try to get more details from the response
                error_data = e.response.json()
                error_message = f"{error_message} - {error_data.get('message', str(error_data))}"
            except (json.JSONDecodeError, AttributeError):
                error_message = f"{error_message} - {e.response.text if hasattr(e, 'response') else str(e)}"

            logger.error(error_message)

            ## Re-raise with more context
            if 400 <= status_code < 500:
                logger.error(f"Client error: {error_message}")
                raise Exception(f"Client error (HTTP {status_code}): {error_message}")
            elif 500 <= status_code < 600:
                logger.error(f"Server error: {error_message}")
                raise Exception(f"Server error (HTTP {status_code}): {error_message}")
            else:
                raise

        except requests.exceptions.ConnectionError as e:
            logger.error(f"Connection Error: {e}")
            raise Exception(f"Connection Error: Could not connect to {url}")

        except requests.exceptions.Timeout as e:
            logger.error(f"Timeout Error: {e}")
            raise Exception(f"Timeout Error: Request to {url} timed out")

        except requests.exceptions.RequestException as e:
            logger.error(f"Request Exception: {e}")
            raise Exception(f"Request Error: {str(e)}")

    def get(self, endpoint, params=None, headers=None):
        """Make a GET request to the API."""
        return self.request(endpoint, method="get", params=params, headers=headers)

    def post(self, endpoint, data=None, headers=None):
        """Make a POST request to the API."""
        return self.request(endpoint, method="post", data=data, headers=headers)

    def put(self, endpoint, data=None, headers=None):
        """Make a PUT request to the API."""
        return self.request(endpoint, method="put", data=data, headers=headers)

    def delete(self, endpoint, headers=None):
        """Make a DELETE request to the API."""
        return self.request(endpoint, method="delete", headers=headers)

## Test the API client with JSONPlaceholder
def test_api_client():
    """Test the API client with JSONPlaceholder."""
    client = APIClient("https://jsonplaceholder.typicode.com")

    try:
        ## Get a list of posts
        print("\nGetting posts:")
        posts = client.get("/posts")
        print(f"Retrieved {len(posts)} posts")
        print(f"First post: {posts[0]}")

        ## Get a single post
        print("\nGetting a single post:")
        post = client.get("/posts/1")
        print(f"Retrieved post: {post}")

        ## Get a non-existent post (404)
        print("\nTrying to get a non-existent post:")
        try:
            client.get("/posts/999999")
        except Exception as e:
            print(f"Expected error: {e}")

        ## Create a new post
        print("\nCreating a new post:")
        new_post = client.post("/posts", {
            "title": "My New Post",
            "body": "This is the content of my new post.",
            "userId": 1
        })
        print(f"Created post: {new_post}")

        ## Update a post
        print("\nUpdating a post:")
        updated_post = client.put("/posts/1", {
            "id": 1,
            "title": "Updated Title",
            "body": "Updated content.",
            "userId": 1
        })
        print(f"Updated post: {updated_post}")

        ## Delete a post
        print("\nDeleting a post:")
        delete_response = client.delete("/posts/1")
        print(f"Delete response: {delete_response}")

    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == "__main__":
    test_api_client()

スクリプトを実行して、完全な API クライアントが様々なシナリオをどのように処理するかを確認します。

python api_client.py

エラーハンドリングのテスト

API クライアントのエラーハンドリング機能をテストするための別のファイルを作成しましょう。http_status_lab ディレクトリに、test_error_handling.py という名前のファイルを作成します。

from api_client import APIClient
import time

def test_different_errors():
    """Test different error scenarios with our API client."""

    ## Test with a valid API
    print("\n1. Testing with a valid API:")
    valid_client = APIClient("https://jsonplaceholder.typicode.com")
    try:
        data = valid_client.get("/posts/1")
        print(f"Success! Retrieved data: {data}")
    except Exception as e:
        print(f"Unexpected error: {e}")

    ## Test with a 404 error
    print("\n2. Testing with a non-existent endpoint (404):")
    try:
        valid_client.get("/non_existent_endpoint")
    except Exception as e:
        print(f"Expected error: {e}")

    ## Test with an invalid host
    print("\n3. Testing with an invalid host (Connection Error):")
    invalid_client = APIClient("https://this-does-not-exist-123456789.com")
    try:
        invalid_client.get("/anything")
    except Exception as e:
        print(f"Expected error: {e}")

    ## Test with a timeout
    print("\n4. Testing with a timeout:")
    timeout_client = APIClient("https://httpbin.org")
    try:
        ## httpbin.org/delay/5 will delay the response for 5 seconds
        ## but we'll set the timeout to 2 seconds
        timeout_client.session.request = lambda **kwargs: timeout_client.session.request_original(
            **{**kwargs, 'timeout': 2}
        )
        timeout_client.session.request_original = timeout_client.session.request
        timeout_client.get("/delay/5")
    except Exception as e:
        print(f"Expected error: {e}")

if __name__ == "__main__":
    test_different_errors()

上記のスクリプトは、実行時にメソッドを変更しようとしているため、タイムアウト処理が原因でエラーが発生する可能性があります。これらの問題を回避するために、これを簡素化しましょう。

from api_client import APIClient

def test_different_errors():
    """Test different error scenarios with our API client."""

    ## Test with a valid API
    print("\n1. Testing with a valid API:")
    valid_client = APIClient("https://jsonplaceholder.typicode.com")
    try:
        data = valid_client.get("/posts/1")
        print(f"Success! Retrieved data: {data}")
    except Exception as e:
        print(f"Unexpected error: {e}")

    ## Test with a 404 error
    print("\n2. Testing with a non-existent endpoint (404):")
    try:
        valid_client.get("/non_existent_endpoint")
    except Exception as e:
        print(f"Expected error: {e}")

    ## Test with a server error
    print("\n3. Testing with a server error (500):")
    error_client = APIClient("https://httpbin.org")
    try:
        error_client.get("/status/500")
    except Exception as e:
        print(f"Expected error: {e}")

if __name__ == "__main__":
    test_different_errors()

エラーテストスクリプトを実行します。

python test_error_handling.py

このスクリプトは、API クライアントが様々なエラーシナリオをどのように処理するかを示し、実際のアプリケーションのための堅牢な基盤を提供します。

主なポイント

この完全な API クライアントを構築することで、いくつかの重要な技術を学びました。

  1. 再利用可能な API クライアントクラスの作成
  2. 包括的なエラーハンドリングの実装
  3. デバッグのためのリクエストとレスポンスのロギング
  4. 様々なレスポンス形式の解析
  5. 様々な HTTP メソッド (GET、POST、PUT、DELETE) の処理

これらのスキルは、Web API と対話する堅牢な Python アプリケーションを構築するために不可欠であり、コードが様々なエラーシナリオを適切に処理し、ユーザーに意味のあるエラーメッセージを提供できるようにします。

まとめ

この実験では、Python のリクエストにおける HTTP ステータスコードの処理方法を学び、基本的なものから高度なテクニックへと進みました。これで、以下のスキルを習得しました。

  • HTTP ステータスコードのさまざまなカテゴリとその意味を理解する
  • ステータスコードチェックによる基本的なエラーハンドリングを実装する
  • 簡単なエラーハンドリングに raise_for_status() を使用する
  • より具体的なエラーハンドリングのためにカスタム例外クラスを作成する
  • 包括的なエラーハンドリングを備えた完全な API クライアントを構築する

これらのスキルは、Web サービスと対話する堅牢な Python アプリケーションを開発するために不可欠です。HTTP ステータスコードとエラーを適切に処理することで、より信頼性の高いアプリケーションを作成し、より良いユーザーエクスペリエンスを提供し、保守を容易にすることができます。

Python アプリケーションで HTTP リクエストを扱う際には、常にエラーハンドリングを考慮してください。これは、プロフェッショナルなコードとアマチュア的な実装を区別する、Web 開発の重要な側面です。