WSGI: A interface de aplicativo de servidor para Python
Publicados: 2022-03-11Em 1993, a web ainda estava em sua infância, com cerca de 14 milhões de usuários e 100 sites. As páginas eram estáticas, mas já havia a necessidade de produzir conteúdo dinâmico, como notícias e dados atualizados. Respondendo a isso, Rob McCool e outros colaboradores implementaram o Common Gateway Interface (CGI) no servidor web HTTPd do National Center for Supercomputing Applications (NCSA) (o precursor do Apache). Este foi o primeiro servidor web que poderia servir conteúdo gerado por um aplicativo separado.
Desde então, o número de usuários na Internet explodiu e sites dinâmicos se tornaram onipresentes. Ao aprender um novo idioma pela primeira vez ou mesmo aprender a codificar, os desenvolvedores, em breve, querem saber como conectar seu código à web.
Python na Web e a ascensão do WSGI
Desde a criação do CGI, muita coisa mudou. A abordagem CGI tornou-se impraticável, pois exigia a criação de um novo processo a cada solicitação, desperdiçando memória e CPU. Algumas outras abordagens de baixo nível surgiram, como FastCGI](http://www.fastcgi.com/) (1996) e mod_python (2000), fornecendo diferentes interfaces entre os frameworks web Python e o servidor web. Com a proliferação de diferentes abordagens, a escolha do framework por parte do desenvolvedor acabou restringindo as escolhas de servidores web e vice-versa.
Para resolver esse problema, em 2003, Phillip J. Eby propôs o PEP-0333, o Python Web Server Gateway Interface (WSGI). A ideia era fornecer uma interface universal de alto nível entre aplicativos Python e servidores web.
Em 2003, o PEP-3333 atualizou a interface WSGI para adicionar suporte ao Python 3. Hoje em dia, quase todos os frameworks Python usam WSGI como meio, se não o único meio, de se comunicar com seus servidores web. É assim que Django, Flask e muitos outros frameworks populares fazem isso.
Este artigo pretende fornecer ao leitor um vislumbre de como o WSGI funciona e permitir que o leitor construa um aplicativo ou servidor WSGI simples. No entanto, não pretende ser exaustivo, e os desenvolvedores que pretendem implementar servidores ou aplicativos prontos para produção devem dar uma olhada mais detalhada na especificação WSGI.
A interface WSGI do Python
O WSGI especifica regras simples que o servidor e o aplicativo devem obedecer. Vamos começar revisando esse padrão geral.
Interface do aplicativo
No Python 3.5, as interfaces do aplicativo são assim:
def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]
No Python 2.7, essa interface não seria muito diferente; a única mudança seria que o corpo é representado por um objeto str
, em vez de um bytes
.
Embora tenhamos usado uma função neste caso, qualquer callable servirá. As regras para o objeto de aplicação aqui são:
- Deve ser um callable com parâmetros
environ
estart_response
. - Deve chamar o retorno de chamada
start_response
antes de enviar o corpo. - Deve retornar um iterável com partes do corpo do documento.
Outro exemplo de objeto que satisfaz essas regras e produziria o mesmo efeito é:
class Application: def __init__(self, environ, start_response): self.environ = environ self.start_response = start_response def __iter__(self): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] self.start_response(status, headers) yield body
Interface do servidor
Um servidor WSGI pode interagir com este aplicativo assim::
def write(chunk): '''Write data back to client''' ... def send_status(status): '''Send HTTP status code''' ... def send_headers(headers): '''Send HTTP headers''' ... def start_response(status, headers): '''WSGI start_response callable''' send_status(status) send_headers(headers) return write # Make request to application response = application(environ, start_response) try: for chunk in response: write(chunk) finally: if hasattr(response, 'close'): response.close()
Como você deve ter notado, o callable start_response
retornou um callable de write
que o aplicativo pode usar para enviar dados de volta ao cliente, mas que não foi usado pelo nosso exemplo de código do aplicativo. Essa interface de write
está obsoleta e podemos ignorá-la por enquanto. Ele será brevemente discutido mais adiante no artigo.
Outra peculiaridade das responsabilidades do servidor é chamar o método opcional close
no iterador de resposta, se existir. Como apontado no artigo de Graham Dumpleton aqui, é um recurso frequentemente negligenciado do WSGI. Chamar esse método, se existir , permite que o aplicativo libere quaisquer recursos que ainda possa conter.
O argumento environ
do aplicativo chamável
O parâmetro environ
deve ser um objeto de dicionário. Ele é usado para passar informações de solicitação e servidor para o aplicativo, da mesma forma que o CGI faz. Na verdade, todas as variáveis de ambiente CGI são válidas no WSGI e o servidor deve passar todas as que se aplicam à aplicação.
Embora existam muitas chaves opcionais que podem ser passadas, várias são obrigatórias. Tomando como exemplo a seguinte requisição GET
:
$ curl 'http://localhost:8000/auth?user=obiwan&token=123'
Estas são as chaves que o servidor deve fornecer e os valores que elas assumiriam:
Chave | Valor | Comentários |
---|---|---|
REQUEST_METHOD | "GET" | |
SCRIPT_NAME | "" | dependente da configuração do servidor |
PATH_INFO | "/auth" | |
QUERY_STRING | "token=123" | |
CONTENT_TYPE | "" | |
CONTENT_LENGTH | "" | |
SERVER_NAME | "127.0.0.1" | dependente da configuração do servidor |
SERVER_PORT | "8000" | |
SERVER_PROTOCOL | "HTTP/1.1" | |
HTTP_(...) | Cabeçalhos HTTP fornecidos pelo cliente | |
wsgi.version | (1, 0) | tupla com versão WSGI |
wsgi.url_scheme | "http" | |
wsgi.input | Objeto semelhante a um arquivo | |
wsgi.errors | Objeto semelhante a um arquivo | |
wsgi.multithread | False | True se o servidor for multithread |
wsgi.multiprocess | False | True se o servidor executar vários processos |
wsgi.run_once | False | True se o servidor espera que este script seja executado apenas uma vez (por exemplo: em um ambiente CGI) |
A exceção a essa regra é que, se uma dessas chaves estiver vazia (como CONTENT_TYPE
na tabela acima), elas poderão ser omitidas do dicionário e será assumido que elas correspondem à string vazia.
wsgi.input
e wsgi.errors
A maioria das chaves de environ
são diretas, mas duas delas merecem um pouco mais de esclarecimento: wsgi.input
, que deve conter um fluxo com o corpo da solicitação do cliente, e wsgi.errors
, onde o aplicativo relata quaisquer erros encontrados. Erros enviados do aplicativo para wsgi.errors
normalmente seriam enviados para o log de erros do servidor.
Essas duas chaves devem conter objetos semelhantes a arquivos; ou seja, objetos que fornecem interfaces para serem lidas ou gravadas como fluxos, assim como o objeto que obtemos quando abrimos um arquivo ou um soquete em Python. Isso pode parecer complicado no começo, mas felizmente, o Python nos dá boas ferramentas para lidar com isso.
Primeiro, de que tipo de fluxos estamos falando? De acordo com a definição do WSGI, wsgi.input
e wsgi.errors
devem manipular objetos bytes
em Python 3 e objetos str
em Python 2. Em ambos os casos, se quisermos usar um buffer na memória para passar ou obter dados através do WSGI interface, podemos usar a classe io.BytesIO
.

Como exemplo, se estivermos escrevendo um servidor WSGI, podemos fornecer o corpo da solicitação para o aplicativo assim:
- Para Python 2.7
import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
- Para Python 3.5
import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)
No lado do aplicativo, se quisermos transformar uma entrada de fluxo recebida em uma string, gostaríamos de escrever algo assim:
- Para Python 2.7
readstr = environ['wsgi.input'].read() # returns str object
- Para Python 3.5
readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object
O fluxo wsgi.errors
deve ser usado para relatar erros do aplicativo ao servidor e as linhas devem ser encerradas por um \n
. O servidor web deve se encarregar de converter para um final de linha diferente de acordo com o sistema.
O argumento start_response
do aplicativo chamável
O argumento start_response
deve ser um callable com dois argumentos obrigatórios, ou seja, status
e headers
, e um argumento opcional, exc_info
. Ele deve ser chamado pelo aplicativo antes que qualquer parte do corpo seja enviada de volta ao servidor web.
No primeiro exemplo de aplicativo no início deste artigo, retornamos o corpo da resposta como uma lista e, portanto, não temos controle sobre quando a lista será iterada. Por causa disso, tivemos que chamar start_response
antes de retornar a lista.
Na segunda, chamamos start_response
antes de produzir a primeira (e, neste caso, apenas) parte do corpo da resposta. De qualquer maneira é válido dentro da especificação WSGI.
Do lado do servidor web, a chamada de start_response
não deve realmente enviar os cabeçalhos para o cliente, mas atrasá-lo até que haja pelo menos uma string de bytes não vazia no corpo da resposta para enviar de volta ao cliente. Esta arquitetura permite que os erros sejam reportados corretamente até o último momento possível de execução da aplicação.
O Argumento de status
de start_response
O argumento de status
passado para o retorno de chamada start_response
deve ser uma string que consiste em um código de status HTTP e uma descrição, separados por um único espaço. Exemplos válidos são: '200 OK'
ou '404 Not Found'
.
O argumento headers
de start_response
O argumento headers
passado para o callback start_response
deve ser uma list
Python de tuple
s, com cada tupla composta como (header_name, header_value)
. Tanto o nome quanto o valor de cada cabeçalho devem ser strings (independentemente da versão do Python). Este é um exemplo raro em que o tipo importa, pois isso é realmente exigido pela especificação WSGI.
Aqui está um exemplo válido de como um argumento de header
pode se parecer:
response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]
Os cabeçalhos HTTP não diferenciam maiúsculas de minúsculas e, se estivermos escrevendo um servidor da Web compatível com WSGI, isso é algo a ser observado ao verificar esses cabeçalhos. Além disso, a lista de cabeçalhos fornecida pelo aplicativo não deve ser exaustiva. É responsabilidade do servidor garantir que todos os cabeçalhos HTTP necessários existam antes de enviar a resposta de volta ao cliente, preenchendo quaisquer cabeçalhos não fornecidos pelo aplicativo.
O argumento exc_info
de start_response
O retorno de chamada start_response
deve suportar um terceiro argumento exc_info
, usado para tratamento de erros. O uso e a implementação corretos desse argumento são de extrema importância para servidores e aplicativos da Web de produção, mas estão fora do escopo deste artigo.
Mais informações sobre ele podem ser obtidas na especificação WSGI, aqui.
O valor de retorno start_response
– O retorno de chamada de write
Para fins de compatibilidade com versões anteriores, os servidores da Web que implementam WSGI devem retornar um callable de write
. Esse retorno de chamada deve permitir que o aplicativo grave dados de resposta do corpo diretamente de volta ao cliente, em vez de cedê-los ao servidor por meio de um iterador.
Apesar de sua presença, esta é uma interface obsoleta e novos aplicativos devem abster-se de usá-la.
Gerando o Corpo de Resposta
Os aplicativos que implementam o WSGI devem gerar o corpo da resposta retornando um objeto iterável. Para a maioria dos aplicativos, o corpo da resposta não é muito grande e cabe facilmente na memória do servidor. Nesse caso, a maneira mais eficiente de enviá-lo é tudo de uma vez, com um elemento iterável. Em casos especiais, onde o carregamento do corpo inteiro na memória é inviável, a aplicação pode devolvê-lo parte a parte através desta interface iterável.
Há apenas uma pequena diferença aqui entre o WSGI do Python 2 e o WSGI do Python 3: no Python 3, o corpo da resposta é representado por objetos bytes
; em Python 2, o tipo correto para isso é str
.
Converter strings UTF-8 em bytes
ou str
é uma tarefa fácil:
- Python 3.5:
body = 'unicode stuff'.encode('utf-8')
- Python 2.7:
body = u'unicode stuff'.encode('utf-8')
Se você quiser saber mais sobre o manuseio de unicode e bytestring do Python 2, há um bom tutorial no YouTube.
Os servidores da Web que implementam WSGI também devem suportar o retorno de chamada de write
para compatibilidade com versões anteriores, conforme descrito acima.
Testando seu aplicativo sem um servidor Web
Com uma compreensão dessa interface simples, podemos criar scripts facilmente para testar nossos aplicativos sem precisar inicializar um servidor.
Tome este pequeno script, por exemplo:
from io import BytesIO def get(app, path = '/', query = ''): response_status = [] response_headers = [] def start_response(status, headers): status = status.split(' ', 1) response_status.append((int(status[0]), status[1])) response_headers.append(dict(headers)) environ = { 'HTTP_ACCEPT': '*/*', 'HTTP_HOST': '127.0.0.1:8000', 'HTTP_USER_AGENT': 'TestAgent/1.0', 'PATH_INFO': path, 'QUERY_STRING': query, 'REQUEST_METHOD': 'GET', 'SERVER_NAME': '127.0.0.1', 'SERVER_PORT': '8000', 'SERVER_PROTOCOL': 'HTTP/1.1', 'SERVER_SOFTWARE': 'TestServer/1.0', 'wsgi.errors': BytesIO(b''), 'wsgi.input': BytesIO(b''), 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0), } response_body = app(environ, start_response) merged_body = ''.join((x.decode('utf-8') for x in response_body)) if hasattr(response_body, 'close'): response_body.close() return {'status': response_status[0], 'headers': response_headers[0], 'body': merged_body}
Dessa forma, podemos, por exemplo, inicializar alguns dados de teste e módulos simulados em nosso aplicativo e fazer chamadas GET
para testar se ele responde adequadamente. Podemos ver que não é um servidor web real, mas interage com nosso aplicativo de maneira comparável, fornecendo ao aplicativo um retorno de chamada start_response
e um dicionário com nossas variáveis de ambiente. Ao final da solicitação, ele consome o iterador do corpo da resposta e retorna uma string com todo o seu conteúdo. Métodos semelhantes (ou gerais) podem ser criados para diferentes tipos de solicitações HTTP.
Embrulhar
Neste artigo, não abordamos como o WSGI lida com uploads de arquivos, pois isso pode ser considerado um recurso mais “avançado”, não adequado para um artigo introdutório. Se você quiser saber mais sobre isso, dê uma olhada na seção PEP-3333 referente ao manuseio de arquivos.
Espero que este artigo seja útil para ajudar a criar uma melhor compreensão de como o Python se comunica com os servidores da Web e permitir que os desenvolvedores usem essa interface de maneiras interessantes e criativas.
Agradecimentos
Gostaria de agradecer ao meu editor Nick McCrea por me ajudar com este artigo. Devido ao seu trabalho, o texto original ficou muito mais claro e vários erros não ficaram sem correção.