WSGI: la interfaz de aplicación de servidor para Python
Publicado: 2022-03-11En 1993, la web aún estaba en sus inicios, con alrededor de 14 millones de usuarios y 100 sitios web. Las páginas eran estáticas, pero ya existía la necesidad de producir contenido dinámico, como noticias y datos actualizados. En respuesta a esto, Rob McCool y otros colaboradores implementaron Common Gateway Interface (CGI) en el servidor web HTTPd del National Center for Supercomputing Applications (NCSA) (el precursor de Apache). Este fue el primer servidor web que podía servir contenido generado por una aplicación separada.
Desde entonces, la cantidad de usuarios en Internet se ha disparado y los sitios web dinámicos se han vuelto omnipresentes. Cuando aprenden por primera vez un nuevo idioma o incluso aprenden a codificar por primera vez, los desarrolladores pronto querrán saber cómo conectar su código a la web.
Python en la web y el surgimiento de WSGI
Desde la creación de CGI, mucho ha cambiado. El enfoque CGI se volvió poco práctico, ya que requería la creación de un nuevo proceso en cada solicitud, desperdiciando memoria y CPU. Surgieron algunos otros enfoques de bajo nivel, como FastCGI](http://www.fastcgi.com/) (1996) y mod_python (2000), que proporcionan diferentes interfaces entre los marcos web de Python y el servidor web. A medida que proliferaban los diferentes enfoques, la elección del marco por parte del desarrollador terminó restringiendo las opciones de los servidores web y viceversa.
Para abordar este problema, en 2003, Phillip J. Eby propuso PEP-0333, la interfaz de puerta de enlace del servidor web de Python (WSGI). La idea era proporcionar una interfaz universal de alto nivel entre las aplicaciones de Python y los servidores web.
En 2003, PEP-3333 actualizó la interfaz WSGI para agregar compatibilidad con Python 3. Hoy en día, casi todos los frameworks de Python usan WSGI como un medio, si no el único medio, para comunicarse con sus servidores web. Así es como lo hacen Django, Flask y muchos otros frameworks populares.
Este artículo tiene la intención de brindarle al lector una idea de cómo funciona WSGI y permitirle construir una aplicación o servidor WSGI simple. Sin embargo, no pretende ser exhaustivo, y los desarrolladores que tengan la intención de implementar servidores o aplicaciones listos para la producción deben analizar más detenidamente la especificación WSGI.
La interfaz WSGI de Python
WSGI especifica reglas simples que el servidor y la aplicación deben cumplir. Comencemos por revisar este patrón general.
Interfaz de la aplicación
En Python 3.5, las interfaces de la aplicación son así:
def application(environ, start_response): body = b'Hello world!\n' status = '200 OK' headers = [('Content-type', 'text/plain')] start_response(status, headers) return [body]
En Python 2.7, esta interfaz no sería muy diferente; el único cambio sería que el cuerpo está representado por un objeto str
, en lugar de bytes
.
Aunque hemos usado una función en este caso, cualquier invocable servirá. Las reglas para el objeto de la aplicación aquí son:
- Debe ser invocable con los parámetros
environ
ystart_response
. - Debe llamar a la devolución de llamada
start_response
antes de enviar el cuerpo. - Debe devolver un iterable con partes del cuerpo del documento.
Otro ejemplo de un objeto que cumple estas reglas y produciría el mismo efecto es:
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
Interfaz del servidor
Un servidor WSGI podría interactuar con esta aplicación de esta manera:
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 habrás notado, el invocable start_response
devolvió un invocable de write
que la aplicación puede usar para enviar datos al cliente, pero que no fue utilizado por nuestro ejemplo de código de aplicación. Esta interfaz de write
está obsoleta y podemos ignorarla por ahora. Se discutirá brevemente más adelante en el artículo.
Otra peculiaridad de las responsabilidades del servidor es llamar al método de close
opcional en el iterador de respuesta, si existe. Como se señala en el artículo de Graham Dumpleton aquí, es una característica de WSGI que a menudo se pasa por alto. Llamar a este método, si existe , permite que la aplicación libere cualquier recurso que aún pueda contener.
El argumento del entorno de Application environ
El parámetro de environ
debe ser un objeto de diccionario. Se utiliza para pasar la solicitud y la información del servidor a la aplicación, de la misma manera que lo hace CGI. De hecho, todas las variables de entorno CGI son válidas en WSGI y el servidor debe pasar todas las que correspondan a la aplicación.
Si bien hay muchas claves opcionales que se pueden pasar, varias son obligatorias. Tomando como ejemplo la siguiente solicitud GET
:
$ curl 'http://localhost:8000/auth?user=obiwan&token=123'
Estas son las claves que debe proporcionar el servidor, y los valores que tomarían:
Llave | Valor | Comentarios |
---|---|---|
REQUEST_METHOD | "GET" | |
SCRIPT_NAME | "" | Depende de la configuración del servidor |
PATH_INFO | "/auth" | |
QUERY_STRING | "token=123" | |
CONTENT_TYPE | "" | |
CONTENT_LENGTH | "" | |
SERVER_NAME | "127.0.0.1" | Depende de la configuración del servidor |
SERVER_PORT | "8000" | |
SERVER_PROTOCOL | "HTTP/1.1" | |
HTTP_(...) | Encabezados HTTP proporcionados por el cliente | |
wsgi.version | (1, 0) | tupla con versión WSGI |
wsgi.url_scheme | "http" | |
wsgi.input | Objeto similar a un archivo | |
wsgi.errors | Objeto similar a un archivo | |
wsgi.multithread | False | True si el servidor es multiproceso |
wsgi.multiprocess | False | True si el servidor ejecuta múltiples procesos |
wsgi.run_once | False | True si el servidor espera que este script se ejecute solo una vez (por ejemplo, en un entorno CGI) |
La excepción a esta regla es que si una de estas claves estuviera vacía (como CONTENT_TYPE
en la tabla anterior), se pueden omitir del diccionario y se supondrá que corresponden a la cadena vacía.
wsgi.input
y wsgi.errors
La mayoría de las claves de wsgi.input
environ
que debe contener un flujo con el cuerpo de la solicitud del cliente, y wsgi.errors
, donde la aplicación informa de cualquier error que encuentre. Los errores enviados desde la aplicación a wsgi.errors
normalmente se enviarían al registro de errores del servidor.
Estas dos claves deben contener objetos similares a archivos; es decir, objetos que proporcionan interfaces para leer o escribir como flujos, al igual que el objeto que obtenemos cuando abrimos un archivo o un socket en Python. Esto puede parecer complicado al principio, pero afortunadamente, Python nos brinda buenas herramientas para manejarlo.
Primero, ¿de qué tipo de flujos estamos hablando? Según la definición de WSGI, wsgi.input
y wsgi.errors
deben manejar objetos bytes
en Python 3 y objetos str
en Python 2. En cualquier caso, si deseamos usar un búfer en memoria para pasar u obtener datos a través de WSGI interfaz, podemos usar la clase io.BytesIO
.

Como ejemplo, si estamos escribiendo un servidor WSGI, podríamos proporcionar el cuerpo de la solicitud a la aplicación de esta manera:
- Para pitón 2.7
import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
- Para pitón 3.5
import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)
En el lado de la aplicación, si quisiéramos convertir una entrada de flujo que recibimos en una cadena, nos gustaría escribir algo como esto:
- Para pitón 2.7
readstr = environ['wsgi.input'].read() # returns str object
- Para pitón 3.5
readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object
El flujo wsgi.errors
debe usarse para informar errores de la aplicación al servidor, y las líneas deben terminar con un \n
. El servidor web debe encargarse de convertir a un final de línea diferente según el sistema.
El argumento start_response
de Application Callable
El argumento start_response
debe ser invocable con dos argumentos obligatorios, a saber, status
y headers
, y un argumento opcional, exc_info
. Debe ser llamado por la aplicación antes de que cualquier parte del cuerpo se envíe de vuelta al servidor web.
En el primer ejemplo de aplicación al comienzo de este artículo, devolvimos el cuerpo de la respuesta como una lista y, por lo tanto, no tenemos control sobre cuándo se repetirá la lista. Debido a esto, tuvimos que llamar a start_response
antes de devolver la lista.
En el segundo, llamamos a start_response
justo antes de generar la primera (y, en este caso, la única) parte del cuerpo de la respuesta. Cualquiera de las dos formas es válida dentro de la especificación WSGI.
Desde el lado del servidor web, la llamada de start_response
en realidad no debería enviar los encabezados al cliente, sino retrasarlo hasta que haya al menos una cadena de bytes no vacía en el cuerpo de la respuesta para enviar al cliente. Esta arquitectura permite que los errores se informen correctamente hasta el último momento posible de la ejecución de la aplicación.
El argumento de status
de start_response
El argumento de status
pasado a la devolución de llamada start_response
debe ser una cadena que consta de un código de estado HTTP y una descripción, separados por un solo espacio. Los ejemplos válidos son: '200 OK'
o '404 Not Found'
.
Los headers
Argumento de start_response
El argumento de headers
pasado a la devolución de llamada start_response
debe ser una list
de Python de tuple
, con cada tupla compuesta como (header_name, header_value)
. Tanto el nombre como el valor de cada encabezado deben ser cadenas (independientemente de la versión de Python). Este es un ejemplo raro en el que el tipo importa, ya que esto es requerido por la especificación WSGI.
Aquí hay un ejemplo válido de cómo puede verse un argumento de header
:
response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]
Los encabezados HTTP no distinguen entre mayúsculas y minúsculas, y si estamos escribiendo un servidor web compatible con WSGI, eso es algo a tener en cuenta al verificar estos encabezados. Además, no se supone que la lista de encabezados proporcionados por la aplicación sea exhaustiva. Es responsabilidad del servidor asegurarse de que existan todos los encabezados HTTP requeridos antes de enviar la respuesta al cliente, completando los encabezados no proporcionados por la aplicación.
El argumento exc_info
de start_response
La devolución de llamada start_response
debe admitir un tercer argumento exc_info
, utilizado para el manejo de errores. El uso correcto y la implementación de este argumento es de suma importancia para los servidores web y las aplicaciones de producción, pero está fuera del alcance de este artículo.
Se puede obtener más información al respecto en la especificación WSGI, aquí.
El valor de retorno start_response
– La devolución de llamada de write
Para fines de compatibilidad con versiones anteriores, los servidores web que implementan WSGI deben devolver una llamada de write
. Esta devolución de llamada debería permitir que la aplicación escriba datos de respuesta del cuerpo directamente al cliente, en lugar de dárselos al servidor a través de un iterador.
A pesar de su presencia, esta es una interfaz obsoleta y las nuevas aplicaciones deben abstenerse de usarla.
Generación del cuerpo de respuesta
Las aplicaciones que implementan WSGI deben generar el cuerpo de la respuesta devolviendo un objeto iterable. Para la mayoría de las aplicaciones, el cuerpo de la respuesta no es muy grande y cabe fácilmente en la memoria del servidor. En ese caso, la forma más eficiente de enviarlo es todo a la vez, con un elemento iterable. En casos especiales, donde no es posible cargar todo el cuerpo en la memoria, la aplicación puede devolverlo parte por parte a través de esta interfaz iterable.
Aquí solo hay una pequeña diferencia entre el WSGI de Python 2 y Python 3: en Python 3, el cuerpo de la respuesta está representado por objetos de bytes
; en Python 2, el tipo correcto para esto es str
.
Convertir cadenas UTF-8 en bytes
o str
es una tarea fácil:
- Pitón 3.5:
body = 'unicode stuff'.encode('utf-8')
- Pitón 2.7:
body = u'unicode stuff'.encode('utf-8')
Si desea obtener más información sobre el manejo de cadenas de bytes y unicode de Python 2, hay un buen tutorial en YouTube.
Los servidores web que implementan WSGI también deben admitir la devolución de llamada de write
para la compatibilidad con versiones anteriores, como se describe anteriormente.
Probar su aplicación sin un servidor web
Con una comprensión de esta sencilla interfaz, podemos crear fácilmente secuencias de comandos para probar nuestras aplicaciones sin necesidad de iniciar un servidor.
Tome este pequeño script, por ejemplo:
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}
De esta manera, podríamos, por ejemplo, inicializar algunos datos de prueba y módulos simulados en nuestra aplicación, y hacer llamadas GET
para probar si responde en consecuencia. Podemos ver que no es un servidor web real, sino que interactúa con nuestra aplicación de una manera comparable proporcionando a la aplicación una devolución de llamada start_response
y un diccionario con nuestras variables de entorno. Al final de la solicitud, consume el iterador del cuerpo de la respuesta y devuelve una cadena con todo su contenido. Se pueden crear métodos similares (o uno general) para diferentes tipos de solicitudes HTTP.
Envolver
En este artículo, no hemos abordado cómo WSGI trata con la carga de archivos, ya que esto podría considerarse una función más "avanzada", no adecuada para un artículo introductorio. Si desea obtener más información al respecto, consulte la sección PEP-3333 que se refiere al manejo de archivos.
Espero que este artículo sea útil para ayudar a crear una mejor comprensión de cómo Python se comunica con los servidores web y permite a los desarrolladores usar esta interfaz de formas interesantes y creativas.
Expresiones de gratitud
Me gustaría agradecer a mi editor Nick McCrea por ayudarme con este artículo. Gracias a su trabajo, el texto original se volvió mucho más claro y varios errores no quedaron sin corregir.