Sitemap
Mikhail Berkov

Mikhail Berkov

WSGI Explained

4 min readMar 2, 2025

--

Press enter or click to view image in full size

If you’ve worked with Python web frameworks like Flask, gunicorn or Django you might have heard that they build on top of WSGI (short for Web Server Gateway Interface). You may have also tried to look at PEP 3333 just to encounter terms like “server gateway” and “application object” and give up.

So let’s dive into WSGI and find out what is going on there.

A Simple WSGI Application

The simplest possible WSGI application looks like this:

from wsgiref.simple_server import make_server

def application(environ, start_response):
response_body = b"Hello, world"
start_response('200 OK', [('Content-Type', 'text/html'), ('Content-Length', str(len(response_body)))])
return [response_body]

httpd = make_server('localhost', 8000, application)
httpd.handle_request()

Save this script under app.py and start the server by running the script as python app.py.

You can now send a request to it like this:

curl -X GET http://localhost:8000/

This will return Hello, world.

What exactly does this code do?

Some Terminology

WSGI is a specification whose job is to create a standard interface between traditional web servers (like nginx) and Python web applications.

The most important WSGI concept is the application object. Interestingly enough, the application object is not really a regular Python object, but a callable. To quote from PEP 3333:

The term “object” should not be misconstrued as requiring an actual object instance: a function, method, class, or instance with a __call__ method are all acceptable for use as an application object.

Really, an application object is just a callable that accepts two arguments — environ and start_response.

The environ variable is a dictionary containing essential information about the request as well as the server configuration. Here is where you can find things like the request method, the query string, and the request headers.

The start_response method is a callback that can be used to initiate an HTTP response. This is what you call when you have finished processing the request, calculated the response data, and want to return the actual response.

The WSGI server (also often referred to as the WSGI gateway) invokes the application object (which, again, is just a callable) once for each request that it receives from an HTTP client. To quote from PEP 3333 again:

The server or gateway invokes the application callable once for each request it receives from an HTTP client, that is directed at the application.

PEP 3333 prescribes very little in terms of actual implementation, and in fact, there are lots of different WSGI implementations out there. Our example uses simple_server.WSGIServer which is available from the standard library.

Note that simple_server.WSGIServer is not a production-ready WSGI implementation. We will cover production-ready WSGI servers in a later article.

The environ Dictionary

As we’ve already mentioned, the environ dictionary contains information about the request and the server configuration.

Get Mikhail Berkov’s stories in your inbox

Join Medium for free to get updates from this writer.

The most important values are:

  • REQUEST_METHOD which contains the HTTP request method (GET in our example)
  • SERVER_NAME which contains the server name (localhost in our example)
  • SERVER_PORT which contains the server port (8000 in our example)
  • SERVER_PROTOCOL which contains the HTTP protocol (most likely HTTP/1.1 in our example)
  • PATH_INFO which contains the request path (/ in our example)
  • QUERY_STRING which contains the query string, i.e. the portion of the URL that contains the query parameters (an empty string in our example)

Additionally, the keys that are prefixed with HTTP_ correspond to the headers of the request. For example, HTTP_USER_AGENT would correspond to the User-Agent header.

Sending a Response

The start_response callable can be used to begin the HTTP response. It takes two mandatory arguments.

The first one is the status code formatted as a status string like 200 OK or 404 Not Found.

The second one represents the response headers and must contain a list of (header_name, header_value) tuples. Each header_name must be a valid HTTP header field-name (like Content-Type or Content-Length).

Here is how this could look like:

start_response('200 OK', [
('Content-Type', 'text/html'),
('Content-Length', str(len(response_body)))
])

You will note that there is an argument that is conspicuously absent here and that’s the actual response body. That’s because start_response only initiates the HTTP response, but it does not actually send it.

Instead, the WSGI application callable returns an iterable (usually a list) containing the response body as byte strings. The WSGI server iterates over this iterable to send the actual content to the client:

def application(environ, start_response):
...
return [response_body]

This is useful because it means that applications can produce response data lazily and generate the content on-the-fly, which is necessary if you want to e.g. stream data.

Reading the Request Body

If you want to read the request body (e.g. in a POST request), you need to access the special wsgi.input field of the environ variable. This is a BufferedReader from which you can read the data using the BufferedReader.read method. Note that this will return bytes, so if you want to get a string, you need to decode those bytes:

request_body = environ['wsgi.input'].read(content_length).decode('utf-8')

You can get the content length from the environ["CONTENT_LENGTH"]. Note that this might be empty so you need to wrap it inside a try...catch:

try:
content_length = int(environ.get('CONTENT_LENGTH', 0))
except ValueError:
content_length = 0

Here is how the full application callable should look like:

def application(environ, start_response):
try:
content_length = int(environ.get('CONTENT_LENGTH', 0))
except ValueError:
content_length = 0
request_body = environ['wsgi.input'].read(content_length).decode('utf-8')
response_body = f"Hello, world\n{request_body}"
start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', str(len(response_body)))])
return [response_body.encode('utf-8')]

--

--