Python requests 에서 다양한 HTTP 상태 코드 처리 방법

PythonBeginner
지금 연습하기

소개

이 튜토리얼은 Python requests 를 사용하여 다양한 HTTP 상태 코드를 처리하는 방법을 안내합니다. HTTP 상태 코드는 웹 요청이 성공했는지 실패했는지, 그리고 다양한 시나리오에 적절하게 응답하는 방법을 이해하는 데 필수적입니다. 이 랩을 마치면 웹 서비스와 상호 작용하는 Python 애플리케이션에서 강력한 오류 처리를 구현하는 방법을 배우게 됩니다.

먼저 HTTP 상태 코드를 이해하는 것으로 시작하여 기본 및 고급 오류 처리 기술을 구현하면서 점차적으로 기술을 쌓아갈 것입니다. 이 지식은 다양한 서버 응답을 우아하게 처리하는 안정적인 Python 웹 애플리케이션을 개발하는 데 매우 중요합니다.

HTTP 상태 코드 이해 및 설정

HTTP 상태 코드란 무엇인가요?

HTTP 상태 코드는 클라이언트의 요청에 대한 응답으로 서버가 반환하는 세 자리 숫자입니다. 이는 특정 HTTP 요청이 성공적으로 완료되었는지 여부를 나타냅니다. 이러한 코드는 다섯 가지 범주로 그룹화됩니다.

  • 1xx (정보): 요청이 수신되었으며 프로세스가 계속 진행 중입니다.
  • 2xx (성공): 요청이 성공적으로 수신, 이해 및 수락되었습니다.
  • 3xx (리디렉션): 요청을 완료하기 위해 추가 조치가 필요합니다.
  • 4xx (클라이언트 오류): 요청에 잘못된 구문이 있거나 충족될 수 없습니다.
  • 5xx (서버 오류): 서버가 유효한 요청을 충족하지 못했습니다.

자주 접하게 될 일반적인 상태 코드는 다음과 같습니다.

상태 코드 이름 설명
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 상태 코드를 테스트할 수 있는 도구를 만들어 보겠습니다. 웹사이트 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 웹 애플리케이션에서 일반적인 패턴으로, 다양한 유형의 응답을 적절하게 처리할 수 있습니다.

고급 오류 처리 구현

이제 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 가 아닌 상태 코드에 대해 자동으로 예외를 발생시키고, 코드가 이를 적절하게 catch 하여 처리할 수 있음을 보여줍니다.

사용자 정의 예외 처리기 만들기

더 복잡한 애플리케이션의 경우, 보다 구체적인 오류 처리를 제공하기 위해 사용자 정의 예외 처리기를 만들 수 있습니다. 더 고급 예제를 만들어 보겠습니다.

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. 다양한 상태 코드에 대한 특정 처리

이러한 접근 방식을 사용하면 더 구조화되고 유지 관리 가능한 방식으로 다양한 오류 유형을 처리할 수 있으며, 이는 더 큰 애플리케이션에 필수적입니다.

완전한 웹 API 클라이언트 구축

이 마지막 단계에서는 배운 모든 것을 결합하여 강력한 오류 처리를 갖춘 완전한 웹 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) 처리

이러한 기술은 웹 API 와 상호 작용하는 강력한 Python 애플리케이션을 구축하는 데 필수적이며, 코드가 다양한 오류 시나리오를 적절하게 처리하고 사용자에게 의미 있는 오류 메시지를 제공할 수 있도록 합니다.

요약

이 Lab 에서는 Python requests 에서 HTTP 상태 코드를 처리하는 방법을 배우고, 기본 기술에서 고급 기술로 발전했습니다. 이제 다음 기술을 갖추게 되었습니다.

  • HTTP 상태 코드의 다양한 범주와 그 의미 이해
  • 상태 코드 검사를 통한 기본 오류 처리 구현
  • 간단한 오류 처리를 위해 raise_for_status() 사용
  • 보다 구체적인 오류 처리를 위한 사용자 정의 예외 클래스 만들기
  • 포괄적인 오류 처리를 갖춘 완전한 API 클라이언트 구축

이러한 기술은 웹 서비스와 상호 작용하는 강력한 Python 애플리케이션을 개발하는 데 필수적입니다. HTTP 상태 코드와 오류를 적절하게 처리함으로써 더 나은 사용자 경험을 제공하고 유지 관리가 더 쉬운, 보다 안정적인 애플리케이션을 만들 수 있습니다.

Python 애플리케이션에서 HTTP 요청으로 작업할 때는 항상 오류 처리를 고려하십시오. 이는 전문적인 코드와 아마추어 구현을 구별하는 웹 개발의 중요한 측면입니다.