WSGI : l'interface serveur-application pour Python

Publié: 2022-03-11

En 1993, le Web en était encore à ses balbutiements, avec environ 14 millions d'utilisateurs et 100 sites Web. Les pages étaient statiques, mais il était déjà nécessaire de produire du contenu dynamique, tel que des actualités et des données à jour. En réponse à cela, Rob McCool et d'autres contributeurs ont implémenté l'interface de passerelle commune (CGI) dans le serveur Web HTTPd du National Center for Supercomputing Applications (NCSA) (le précurseur d'Apache). Il s'agissait du premier serveur Web capable de diffuser du contenu généré par une application distincte.

Depuis, le nombre d'utilisateurs d'Internet a explosé et les sites Web dynamiques sont devenus omniprésents. Lorsqu'ils apprennent un nouveau langage pour la première fois ou même à coder pour la première fois, les développeurs veulent assez tôt savoir comment accrocher leur code au Web.

Python sur le Web et l'essor de WSGI

Depuis la création de CGI, beaucoup de choses ont changé. L'approche CGI est devenue peu pratique, car elle nécessitait la création d'un nouveau processus à chaque requête, gaspillant de la mémoire et du processeur. D'autres approches de bas niveau ont émergé, comme FastCGI](http://www.fastcgi.com/) (1996) et mod_python (2000), fournissant différentes interfaces entre les frameworks Web Python et le serveur Web. Au fur et à mesure que les différentes approches proliféraient, le choix du framework par le développeur a fini par restreindre les choix de serveurs web et vice versa.

Pour résoudre ce problème, en 2003, Phillip J. Eby a proposé PEP-0333, l'interface de passerelle de serveur Web Python (WSGI). L'idée était de fournir une interface universelle de haut niveau entre les applications Python et les serveurs Web.

En 2003, PEP-3333 a mis à jour l'interface WSGI pour ajouter la prise en charge de Python 3. De nos jours, presque tous les frameworks Python utilisent WSGI comme moyen, sinon le seul moyen, de communiquer avec leurs serveurs Web. C'est ainsi que Django, Flask et de nombreux autres frameworks populaires le font.

Cet article a pour but de fournir au lecteur un aperçu du fonctionnement de WSGI et de lui permettre de créer une application ou un serveur WSGI simple. Il n'est pas censé être exhaustif, cependant, et les développeurs ayant l'intention d'implémenter des serveurs ou des applications prêts pour la production devraient examiner de plus près la spécification WSGI.

L'interface Python WSGI

WSGI spécifie des règles simples auxquelles le serveur et l'application doivent se conformer. Commençons par examiner ce schéma général.

L'interface serveur-application Python WSGI.

Interface d'application

Dans Python 3.5, les interfaces d'application ressemblent à ceci :

 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, cette interface ne serait pas très différente ; le seul changement serait que le corps est représenté par un objet str , au lieu d'un bytes .

Bien que nous ayons utilisé une fonction dans ce cas, tout appelable fera l'affaire. Les règles pour l'objet d'application ici sont :

  • Doit être un appelable avec start_response environ
  • Doit appeler le rappel start_response avant d'envoyer le corps.
  • Doit retourner un itérable avec des morceaux du corps du document.

Un autre exemple d'objet qui satisfait à ces règles et produirait le même effet est :

 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 serveur

Un serveur WSGI peut s'interfacer avec cette application comme ceci ::

 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()

Comme vous l'avez peut-être remarqué, l'appelable start_response a renvoyé un appelable en write que l'application peut utiliser pour renvoyer des données au client, mais qui n'a pas été utilisé par notre exemple de code d'application. Cette interface d' write est obsolète et nous pouvons l'ignorer pour le moment. Il sera brièvement discuté plus loin dans l'article.

Une autre particularité des responsabilités du serveur est d'appeler la méthode close optionnelle sur l'itérateur de réponse, si elle existe. Comme indiqué dans l'article de Graham Dumpleton ici, il s'agit d'une fonctionnalité souvent négligée de WSGI. L'appel de cette méthode, si elle existe , permet à l'application de libérer toutes les ressources qu'elle peut encore détenir.

Argument de l' environ de l'application Callable

Le paramètre environ doit être un objet dictionnaire. Il est utilisé pour transmettre les informations de requête et de serveur à l'application, de la même manière que CGI le fait. En fait, toutes les variables d'environnement CGI sont valides dans WSGI et le serveur doit transmettre toutes celles qui s'appliquent à l'application.

Bien qu'il existe de nombreuses clés facultatives pouvant être transmises, plusieurs sont obligatoires. Prenons comme exemple la requête GET suivante :

 $ curl 'http://localhost:8000/auth?user=obiwan&token=123'

Voici les clés que le serveur doit fournir, et les valeurs qu'elles prendraient :

Clé Évaluer commentaires
REQUEST_METHOD "GET"
SCRIPT_NAME "" dépend de la configuration du serveur
PATH_INFO "/auth"
QUERY_STRING "token=123"
CONTENT_TYPE ""
CONTENT_LENGTH ""
SERVER_NAME "127.0.0.1" dépend de la configuration du serveur
SERVER_PORT "8000"
SERVER_PROTOCOL "HTTP/1.1"
HTTP_(...) En-têtes HTTP fournis par le client
wsgi.version (1, 0) tuple avec la version WSGI
wsgi.url_scheme "http"
wsgi.input Objet semblable à un fichier
wsgi.errors Objet semblable à un fichier
wsgi.multithread False True si le serveur est multithread
wsgi.multiprocess False True si le serveur exécute plusieurs processus
wsgi.run_once False True si le serveur s'attend à ce que ce script ne s'exécute qu'une seule fois (par exemple : dans un environnement CGI)

L'exception à cette règle est que si l'une de ces clés devait être vide (comme CONTENT_TYPE dans le tableau ci-dessus), alors elles peuvent être omises du dictionnaire, et on supposera qu'elles correspondent à la chaîne vide.

wsgi.input et wsgi.errors

La plupart des clés d' environ sont simples, mais deux d'entre elles méritent un peu plus de précisions : wsgi.input , qui doit contenir un flux avec le corps de la requête du client, et wsgi.errors , où l'application signale les erreurs qu'elle rencontre. Les erreurs envoyées de l'application à wsgi.errors généralement envoyées au journal des erreurs du serveur.

Ces deux clés doivent contenir des objets de type fichier ; c'est-à-dire des objets qui fournissent des interfaces à lire ou à écrire sous forme de flux, tout comme l'objet que nous obtenons lorsque nous ouvrons un fichier ou un socket en Python. Cela peut sembler délicat au début, mais heureusement, Python nous donne de bons outils pour gérer cela.

Tout d'abord, de quel type de flux parlons-nous ? Selon la définition WSGI, wsgi.input et wsgi.errors doivent gérer les objets bytes en Python 3 et les objets str en Python 2. Dans les deux cas, si nous souhaitons utiliser un tampon en mémoire pour transmettre ou obtenir des données via le WSGI interface, nous pouvons utiliser la classe io.BytesIO .

Par exemple, si nous écrivons un serveur WSGI, nous pourrions fournir le corps de la requête à l'application comme ceci :

  • Pour Python 2.7
 import io ... request_data = 'some request body' environ['wsgi.input'] = io.BytesIO(request_data)
  • Pour Python 3.5
 import io ... request_data = 'some request body'.encode('utf-8') # bytes object environ['wsgi.input'] = io.BytesIO(request_data)

Du côté de l'application, si nous voulions transformer une entrée de flux que nous avons reçue en une chaîne, nous voudrions écrire quelque chose comme ceci :

  • Pour Python 2.7
 readstr = environ['wsgi.input'].read() # returns str object
  • Pour Python 3.5
 readbytes = environ['wsgi.input'].read() # returns bytes object readstr = readbytes.decode('utf-8') # returns str object

Le flux wsgi.errors doit être utilisé pour signaler les erreurs d'application au serveur et les lignes doivent se terminer par un \n . Le serveur Web doit s'occuper de la conversion vers une fin de ligne différente selon le système.

Argument start_response de l'application Callable

L'argument start_response doit être un appelable avec deux arguments obligatoires, à savoir status et headers , et un argument facultatif, exc_info . Il doit être appelé par l'application avant qu'une partie du corps ne soit renvoyée au serveur Web.

Dans le premier exemple d'application au début de cet article, nous avons renvoyé le corps de la réponse sous forme de liste et, par conséquent, nous n'avons aucun contrôle sur le moment où la liste sera itérée. Pour cette raison, nous avons dû appeler start_response avant de renvoyer la liste.

Dans le second, nous avons appelé start_response juste avant de produire le premier (et, dans ce cas, le seul) élément du corps de la réponse. L'une ou l'autre manière est valide dans la spécification WSGI.

Du côté du serveur Web, l'appel de start_response ne doit pas réellement envoyer les en-têtes au client, mais le retarder jusqu'à ce qu'il y ait au moins une chaîne d'octets non vide dans le corps de la réponse à renvoyer au client. Cette architecture permet de remonter correctement les erreurs jusqu'au tout dernier moment possible de l'exécution de l'application.

L'argument de status de start_response

L'argument d' status transmis au rappel start_response doit être une chaîne composée d'un code d'état HTTP et d'une description, séparés par un espace unique. Des exemples valides sont : '200 OK' ou '404 Not Found' .

Les headers Argument de start_response

L'argument headers passé au rappel start_response doit être une list Python de tuple s, avec chaque tuple composé comme (header_name, header_value) . Le nom et la valeur de chaque en-tête doivent être des chaînes (quelle que soit la version de Python). Il s'agit d'un exemple rare dans lequel le type compte, car cela est en effet requis par la spécification WSGI.

Voici un exemple valide de ce à quoi un argument d' header peut ressembler :

 response_body = json.dumps(data).encode('utf-8') headers = [('Content-Type', 'application/json'), ('Content-Length', str(len(response_body))]

Les en-têtes HTTP ne sont pas sensibles à la casse, et si nous écrivons un serveur Web conforme à WSGI, c'est quelque chose à prendre en compte lors de la vérification de ces en-têtes. De plus, la liste des en-têtes fournie par l'application n'est pas censée être exhaustive. Il est de la responsabilité du serveur de s'assurer que tous les en-têtes HTTP requis existent avant de renvoyer la réponse au client, en remplissant tous les en-têtes non fournis par l'application.

L'argument exc_info de start_response

Le rappel start_response doit prendre en charge un troisième argument exc_info , utilisé pour la gestion des erreurs. L'utilisation et la mise en œuvre correctes de cet argument sont de la plus haute importance pour les serveurs Web et les applications de production, mais sortent du cadre de cet article.

De plus amples informations à ce sujet peuvent être obtenues dans la spécification WSGI, ici.

La valeur de retour start_response - Le rappel d' write

À des fins de rétrocompatibilité, les serveurs Web implémentant WSGI doivent renvoyer un callable en write . Ce rappel doit permettre à l'application d'écrire les données de réponse du corps directement au client, au lieu de les transmettre au serveur via un itérateur.

Malgré sa présence, il s'agit d'une interface obsolète et les nouvelles applications doivent s'abstenir de l'utiliser.

Génération du corps de la réponse

Les applications implémentant WSGI doivent générer le corps de la réponse en renvoyant un objet itérable. Pour la plupart des applications, le corps de la réponse n'est pas très volumineux et tient facilement dans la mémoire du serveur. Dans ce cas, le moyen le plus efficace de l'envoyer est tout à la fois, avec un itérable à un élément. Dans des cas particuliers, où le chargement du corps entier en mémoire est impossible, l'application peut le renvoyer partie par partie via cette interface itérable.

Il n'y a qu'une petite différence entre les WSGI de Python 2 et Python 3 : dans Python 3, le corps de la réponse est représenté par des objets bytes ; en Python 2, le type correct pour cela est str .

Convertir des chaînes UTF-8 en bytes ou str est une tâche facile :

  • Python 3.5 :
 body = 'unicode stuff'.encode('utf-8')
  • Python 2.7 :
 body = u'unicode stuff'.encode('utf-8')

Si vous souhaitez en savoir plus sur la gestion de l'unicode et des chaînes d'octets de Python 2, il existe un joli tutoriel sur YouTube.

Les serveurs Web implémentant WSGI doivent également prendre en charge le rappel d' write pour une compatibilité descendante, comme décrit ci-dessus.

Tester votre application sans serveur Web

Avec une compréhension de cette interface simple, nous pouvons facilement créer des scripts pour tester nos applications sans avoir besoin de démarrer un serveur.

Prenez ce petit script, par exemple :

 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 cette façon, nous pourrions, par exemple, initialiser des données de test et des modules fictifs dans notre application, et effectuer des appels GET afin de tester si elle répond en conséquence. Nous pouvons voir qu'il ne s'agit pas d'un véritable serveur Web, mais qu'il s'interface avec notre application de manière comparable en fournissant à l'application un rappel start_response et un dictionnaire avec nos variables d'environnement. À la fin de la requête, il consomme l'itérateur de corps de réponse et renvoie une chaîne avec tout son contenu. Des méthodes similaires (ou générales) peuvent être créées pour différents types de requêtes HTTP.

Emballer

WSGI est un élément essentiel de presque tous les frameworks Web Python.

Dans cet article, nous n'avons pas abordé la manière dont WSGI traite les téléchargements de fichiers, car cela pourrait être considéré comme une fonctionnalité plus "avancée", ne convenant pas à un article d'introduction. Si vous souhaitez en savoir plus, consultez la section PEP-3333 relative à la gestion des fichiers.

J'espère que cet article sera utile pour aider à mieux comprendre comment Python communique avec les serveurs Web et permettra aux développeurs d'utiliser cette interface de manière intéressante et créative.

Remerciements

J'aimerais remercier mon éditeur Nick McCrea de m'avoir aidé avec cet article. Grâce à son travail, le texte original est devenu beaucoup plus clair et plusieurs erreurs ne sont pas restées sans être corrigées.