PEP 3333은 WSGI에 대해서 설명하고 있는 PEP 문서이다, 원 버전은 PEP 0333인데, python 3.x의 지원과 community errata addenda clarifications을 추가한 문서이다.


Overview

WSGI는 “server” 혹은 “gateway” 측과 “application”, “framework” 두가지로 나누어진다. server side에서는 appliction side에서 제공된 object를호출한다. 이 런 object들이 어떻게 제공되는지에 대한 spec은 server side에 나온다. 추가로, 순수한 server/gateway와 application/frameworks는 middleware를 생성해서 양측에서 구현할 수 있다.

A Note On String Types

Python은 버전 별로 String의 구현이 조금씩 다른데, 이를 위해서 각각의 구현체를 어떤 상황에 사용해 두어야하는지 WSGI에서는 정의하고 있다.

  • Native(str) request/response헤더, metadata에서 사용한다.

  • Bytestrings(Python3: bytes, 2: str) request/response의 entity body에서 사용한다.

Application/Framework side


Application object는 두개의 매개변수를 받는 callable object이다. 아래에 function, class를 이용해서 구현한 예시가 있다.

1
2
3
4
5
6
7
8
HELLO_WORLD = b"Hello world!\n"

def simple_app(environ, start_response):
    """Simplest possible application object"""
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    start_response(status, response_headers)
    return [HELLO_WORLD]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class AppClass:
"""Produce the same output, but using a class

(Note: 'AppClass' is the "application" here, so calling it
returns an instance of 'AppClass', which is then the iterable
return value of the "application callable" as required by
the spec.

If we wanted to use *instances* of 'AppClass' as application
objects instead, we would have to implement a '__call__'
method, which would be invoked to execute the application,
and we would need to create an instance for use by the
server or gateway.
"""

def __init__(self, environ, start_response):
    self.environ = environ
    self.start = start_response

def __iter__(self):
    status = '200 OK'
    response_headers = [('Content-type', 'text/plain')]
    self.start(status, response_headers)
    yield HELLO_WORLD

application은 두개의 위치성 매개변수를 가지고 있다.(python에서 매개변수의 위치로 호출하는 방식), 위에서는 environ과 start_response를 사용했는데 이 두 이름을 사용할 필요는 없다, 따라서 server side에서 구현할 때, keyword 방식으로 호출하면 안된다.

environ은 CGI 환경에 대한 변수와 WSGI에서 필요로하는 변수를 갖고 있는 dictinary object이다. 이는 python builtin dictinary를 사용해야만한다.(subclass, UserDict등을 사용하면 안된다.)

start_response는 두개의 위치성 매개변수와 하나의 optional 매개변수를 필요로 하는 callable이다.(status, response_headers, exc_info) 그리고 application은 이 callable을 무조건 호출해야한다. status는 “999 Message here”형태의 str이고, response_headers는 (header_name, header_value) tuple들의 list이다. exc_info parameter는 에러 메세지들을 보여주는데에 사용된다. 자세한 내용은 밑에 있다.

Server/Gateway side


server or gateway side에서는 application을 HTTP client에 의해 일어난 각각의 request에 대해서 한번씩 호출한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import os, sys

enc, esc = sys.getfilesystemencoding(), 'surrogateescape'

def unicode_to_wsgi(u):
    # Convert an environment variable to a WSGI "bytes-as-unicode" string
    return u.encode(enc, esc).decode('iso-8859-1')

def wsgi_to_bytes(s):
    return s.encode('iso-8859-1')

def run_with_cgi(application):
    environ = {k: unicode_to_wsgi(v) for k,v in os.environ.items()}
    environ['wsgi.input']        = sys.stdin.buffer
    environ['wsgi.errors']       = sys.stderr
    environ['wsgi.version']      = (1, 0)
    environ['wsgi.multithread']  = False
    environ['wsgi.multiprocess'] = True
    environ['wsgi.run_once']     = True

    if environ.get('HTTPS', 'off') in ('on', '1'):
        environ['wsgi.url_scheme'] = 'https'
    else:
        environ['wsgi.url_scheme'] = 'http'

    headers_set = []
    headers_sent = []

    def write(data):
        out = sys.stdout.buffer

        if not headers_set:
             raise AssertionError("write() before start_response()")

        elif not headers_sent:
             # Before the first output, send the stored headers
             status, response_headers = headers_sent[:] = headers_set
             out.write(wsgi_to_bytes('Status: %s\r\n' % status))
             for header in response_headers:
                 out.write(wsgi_to_bytes('%s: %s\r\n' % header))
             out.write(wsgi_to_bytes('\r\n'))

        out.write(data)
        out.flush()

    def start_response(status, response_headers, exc_info=None):
        if exc_info:
            try:
                if headers_sent:
                    # Re-raise original exception if headers sent
                    raise exc_info[1].with_traceback(exc_info[2])
            finally:
                exc_info = None     # avoid dangling circular ref
        elif headers_set:
            raise AssertionError("Headers already set!")

        headers_set[:] = [status, response_headers]

        # Note: error checking on the headers should happen here,
        # *after* the headers are set.  That way, if an error
        # occurs, start_response can only be re-called with
        # exc_info set.

        return write

    result = application(environ, start_response)
    try:
        for data in result:
            if data:    # don't send headers until body appears
                write(data)
        if not headers_sent:
            write('')   # send headers now if body was empty
    finally:
        if hasattr(result, 'close'):
            result.close()

start_response는 callable인 write를 return해야한다. write는 bytestring형인 data를 하나 갖고 이를 HTTP response body에 쓸 수 있어야한다.

Middleware: Components that Play Both Sides


middleware로는 다음과 같은 기능을 할 수 있다.

  • target URL에 기반하여, environ을 rewrite하고 서로 다른 application object를 호출 하도록 라우팅 시킨다.
  • 하나의 process에서 side-by-side로 다중의 applications 혹은 frameworks가 동작할 수 있도록 한다.
  • Load balancing과 remote processing
  • content의 생성 후 변형을 구현할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
from piglatin import piglatin

class LatinIter:

    """Transform iterated output to piglatin, if it's okay to do so

    Note that the "okayness" can change until the application yields
    its first non-empty bytestring, so 'transform_ok' has to be a mutable
    truth value.
    """

    def __init__(self, result, transform_ok):
        if hasattr(result, 'close'):
            self.close = result.close
        self._next = iter(result).__next__
        self.transform_ok = transform_ok

    def __iter__(self):
        return self

    def __next__(self):
        if self.transform_ok:
            return piglatin(self._next())   # call must be byte-safe on Py3
        else:
            return self._next()

class Latinator:

    # by default, don't transform output
    transform = False

    def __init__(self, application):
        self.application = application

    def __call__(self, environ, start_response):

        transform_ok = []

        def start_latin(status, response_headers, exc_info=None):

            # Reset ok flag, in case this is a repeat call
            del transform_ok[:]

            for name, value in response_headers:
                if name.lower() == 'content-type' and value == 'text/plain':
                    transform_ok.append(True)
                    # Strip content-length if present, else it'll be wrong
                    response_headers = [(name, value)
                        for name, value in response_headers
                            if name.lower() != 'content-length'
                    ]
                    break

            write = start_response(status, response_headers, exc_info)

            if transform_ok:
                def write_latin(data):
                    write(piglatin(data))   # call must be byte-safe on Py3
                return write_latin
            else:
                return write

        return LatinIter(self.application(environ, start_latin), transform_ok)


# Run foo_app under a Latinator's control, using the example CGI gateway
from foo_app import foo_app
run_with_cgi(Latinator(foo_app))

Specification Details

environ

아래의 값들은 CGI style의 environ dictinary의 이름들이다:

REQUEST_METHOD HTTP의 GET/POST같은 메서드를 말한다.

SCRIPT_NAME URL의 path의 처음위치를 말한다. 비어있는 string인 경우에는 root에 해당한다.

PATH_INFO URL의 path에서 남은부분을 말한다. 비어있는 string인 경우에는 root 경로이고 / 뒤에 아무것도 없을 때다.

QUERY_STRING ? 뒤에 들어오는 문자들이다. 비어있거나 없을 수 있다.

CONTENT_TYPE header로 부터 받아온 content-type이 포함되어 있다. 비어있거나 없을 수 있다.

CONTENT_LENGTH header로 부터 받아온 Content-Length를 말한다. 비어있거나 없을 수 있다.

SERVER_NAME , SERVER_PORT PATH_INFO와 SCRIPT_NAME을 합치면 완전한 URL을 얻을 수 있다. 그러나 HTTP_HOST가 있다면 request url을 얻기위해서 SERVER_NAME을 사용해야한다.

SERVER_PROTOCOL 클라이언트가 보낸 요청의 프로토콜 버전을 말한다. HTTP/1.0 같은 형태이다, 의미상 REQUEST_PROTOCOL이지만 기존의 CGI와 호환을 위하여 이 이름을 사용한다.

HTTP_ Variables HTTP_ 로 시작하는 값들을 말하는데 client로 부터 받은 HTTP 요청 헤더를 의미한다.

아래의 값들은 wsgi 표준에서 명시되는 지원하는 environ dictinary의 이름들이다:

wsgi.version (1, 0) 튜플로 WSGI 1.0 버전을 의미한다.

wsgi.url_scheme URL의 shceme에 대해서 말하는 값으로 http나 https로 표시된다.

wsgi.input HTTP request body로 부터 받아오는 input stream이다.

wsgi.errors error내용을 쓸 수 있는 output stream이다. text만 가능하고 line ending으로 “\n”을 사용해야한다.

wsgi.multithread application object를 multithread를 사용하여 request르 동시에 처리하고 있다면 True여야한다.

wsgi.multiprocess application object를 multiprocess를 사용하여 request르 동시에 처리하고 있다면 True여야한다.

wsgi.run_once request의 처리가 application object를 한번만 실행하고 있는 경우에 True로 되어 있다.

위에서 말한 input과 error스트림은 다음과 같은 method를 이용하여 값을 읽거나 쓸수 있다:

input

read(size)
readline()
readlines(hint)
__iter__()

errors

flush()
write(str)
writelines(seq)