본문으로 바로가기

ASGI에서 HTTP Request 다루는 방법

category Coding/Python 2022. 11. 24. 21:11
반응형

ASGI with HTTP Reqeust

이전 글(https://www.hides.kr/1111)에서 ASGI 인터페이스의 기초에 대해 설명했다. 이제 HTTP Request의 메시지 구조에 대해 좀 더 자세히 살펴보고 ASGI에서 HTTP Request를 다루기 위해 Starlette 라이브러리에서 제공하는 data structure를 어떻게 사용할 수 있는지에 대해 알아본다.

ASGI 어플리케이션에서 발생하는 첫 번째 일은 Request가 들어올 때 초기 정보들을 담은 Scope 딕셔너리가 인스턴스화 된다는 것이다. 아래 예제는 간단한 HTTP Request에 대해 Scope가 어떠한 형태로 구성되는지를 나타낸다.

>>> scope = {
    "type": "http",
    "http_version": "1.1",
    "method": "GET",
    "scheme": "https",
    "path": "/",
    "query_string": b"search=red+blue&maximum_price=20",
    "headers": [
        (b"host", b"www.example.org"),
        (b"accept", b"application/json")
    ],
    "client": ("134.56.78.4", 1453),
    "server": ("www.example.org", 443)
}

Scope 딕셔너리는 WSGI의 environ 딕셔너리와 굉장히 유사하다.

environ = {
    "REQUEST_METHOD": "GET",
    "SCRIPT_NAME": "",
    "PATH_INFO": "/",
    "QUERY_STRING": "search=red+blue&maximum_price=20",
    "SERVER_NAME": "www.example.org",
    "SERVER_PORT": 443,
    "REMOTE_HOST": "134.56.78.4",
    "REMOTE_PORT": 1453,
    "SERVER_PROTOCOL": "HTTP/1.1",
    "HTTP_HOST": "www.example.org",
    "HTTP_ACCEPT": "application/json",
}

Scope Type

ASGI와 WSGI의 가장 근본적인 차이점은 ASGI는 범용적인 메시지 인터페이스인 반면 WSGI는 요청/응답을 중점적으로 다룬다는 점이다. 모든 ASGI의 scope에는 type이라는 키를 통해 프로토콜을 나타내야한다.

scope = {
    "type": "http",  # Deal with an incoming HTTP request.
    ...

Request URL

모든 요청에 대한 full URL은 scheme, server, path, query_string를 통해 만들어질 수 있다. Starlette 프레임워크는 의도적으로 의존성이 없도록 설계되었기 때문에 모든 종류의 ASGI 어플리케이션이나 High level 프레임워크의 기반으로 사용할 수 있다.

>>> from starlette.datastructures import URL
>>> url = URL(scope=scope)
>>> url
URL('https://www.example.org/?search=red+blue&maximum_price=20')
>>> str(url)
'https://www.example.org/?search=red+blue&maximum_price=20'

URL 인스턴스를 사용하면 urlparse와 비슷하게 URL의 다양한 요소들을 확인할 수 있다.

>>> url.scheme
'https'

물론 새로운 값으로 수정할 수도 있다.

>>> url.replace(hostname='www.example.com')
URL('https://www.example.com/?search=red&maximum_price=20')

Request headers

ASGI의 HTTP 헤더는 헤더의 이름과 값을 나타내는 바이트 타입의 쌍으로 구성되어있다. 또한 대소문자를 구분하지 않기 때문에 소문자로 강제한다. 헤더는 같은 이름으로 여러개의 값을 포함할 수 있다. (예를 들어 Set-Cookie) Starlette은 불변이고 대소문자를 구분하지 않는 딕셔너리를 통해 HTTP 헤더를 제공한다.

>>> from starlette.datastructures import Headers
>>> headers = Headers(scope=scope)
>>> headers
Headers({'host': 'www.example.org', 'accept': 'application/json'})

Headers를 인스턴스화하는 비용은 매우 작고 내부 요소들을 확인할 수 있는 편리한 기능을 제공하기 때문에 Request 헤더를 모두 순회하며 값을 찾는 것보다 더 나은 방법이다.

>>> headers['Accept']
'application/json'

응답 헤더의 경우 Starlette은 MutableHeaders 라는 데이터 구조를 제공한다.

Request query params

쿼리 파라미터를 다루기 위해서는 불변 객체인 QueryParams 클래스를 사용한다.

>>> from starlette.datastructures import QueryParams
>>> params = QueryParams(scope=scope)
>>> params
QueryParams(query_string='search=red+blue&maximum_price=20')
>>> params['search']
'red blue'

Request body

HTTP 요청에서 대부분의 정보는 scope에 저장되며 ASGI 어플리케이션이 인스턴스화되는 시점에 세팅된다. 그러나 Request body의 경우 불가능하다. Request body에 접근하기 위해서는 receive를 통해 메시지의 스트림을 가져와야한다. 아래의 코드는 스트림에 있는 단일 메시지값을 추출하는 코드이다.

message = await receive()

그리고 아래 예제는 Request body가 어떠한 형태로 구성되어있는지를 나타낸다.

{
    'type': 'http.request.body',
    'body': b'{"example": "Some JSON data"}',
    'more_body': False
}

만약 직접 ASGI에서 Request body를 가져오고 싶다면 아래의 코드를 사용한다.

body = b''
more_body = True
while more_body:
    message = await receive()
    body += message.get('body', b'')
    more_body = message.get('more_body', False)

Starlette은 HTTP Request에서 scope와 receive를 래핑하여 보다 간단하게 접근할 수 있는 방법을 제공한다.

request = Request(scope, receive=receive)
body = await request.body()

JSON형태로도 파싱할 수 있다.

request = Request(scope, receive=receive)
body = await request.json()

크기가 큰 요청의 경우 모든 Request body를 한번에 메모리에 올리고 싶지 않을수도 있다. 파이썬 3.6부터 제공하는 비동기 제너레이터를 사용하면 이를 해결할 수 있다.

request = Request(scope, receive=receive)

async for chunk in request.stream():
    ... # Do something with "chunk"

파일 업로드와 같은 Multipart요청의 경우 일반적으로 모든 데이터를 메모리에 올리지 않고도 요청에 대한 분석을 할 수 있도록 업로드하는 파일을 디스크의 임시파일로 확인할 수 있다.

request = Request(scope, receive=receive)

# Any upload files in the request body will be streamed into temporary files.
form = await request.form()

 

Reference

https://www.encode.io/articles/working-with-http-requests-in-asgi

반응형

'Coding > Python' 카테고리의 다른 글

ASGI와 HTTP  (0) 2022.11.24
FastAPI BaseHTTPMiddleware 관련 이슈  (0) 2022.07.23
FastAPI add_middleware() 관련 이슈  (0) 2022.07.23
Python Generic Type  (0) 2022.07.07
Python Metaclass 사용해보기  (0) 2022.07.06