Skip to content

Document start_response parameters in wsgiref functions #127522

@gagath

Description

@gagath

Bug report

Bug description:

The example WSGI server in the documentation sets the headers as a list in the simple_app function.

from wsgiref.simple_server import make_server


def hello_world_app(environ, start_response):
    status = "200 OK"  # HTTP Status
    headers = [("Content-type", "text/plain; charset=utf-8")]  # HTTP Headers
    start_response(status, headers)

    # The returned object is going to be printed
    return [b"Hello World"]

with make_server("", 8000, hello_world_app) as httpd:
    print("Serving on port 8000...")

    # Serve until process is killed
    httpd.serve_forever()

When moving the headers from the function to a global (as these headers might not change), this global array of tuples will be modified by the calls to start_response. This is because this function creates a wsgiref.headers.Headers with the passed reference of the global variable, instead of using a copy.

from wsgiref.simple_server import make_server

count = 10
increase = True
headers = [('Content-type', 'text/plain; charset=utf-8')]

def bug_reproducer_app(environ, start_response):
    status = '200 OK'

    start_response(status, headers)

    # Something that will change its Content-Length on every request
    global count
    count -= 1
    if count == 0:
        count = 10
    return [b"Hello " + (b"x" * count)]

with make_server('', 8000, bug_reproducer_app) as httpd:
    print("Serving on port 8000...")
    httpd.serve_forever()

This results the Content-Length value being set once but never updated, resulting in too-short or too-long answers as shown by curl:

$ # First request will set the Content-Length just fine
$ curl localhost:8000 -v
* Host localhost:8000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8000...
* connect to ::1 port 8000 from ::1 port 60888 failed: Connection refused
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.5.0
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Mon, 02 Dec 2024 16:34:16 GMT
< Server: WSGIServer/0.2 CPython/3.12.3
< Content-type: text/plain; charset=utf-8
< Content-Length: 15
<
* Closing connection
Hello xxxxxxxxx%     
$ # Second request will reuse the previous Content-Length set in the global variable
$ curl localhost:8000 -v
* Host localhost:8000 was resolved.
* IPv6: ::1
* IPv4: 127.0.0.1
*   Trying [::1]:8000...
* connect to ::1 port 8000 from ::1 port 60462 failed: Connection refused
*   Trying 127.0.0.1:8000...
* Connected to localhost (127.0.0.1) port 8000
> GET / HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/8.5.0
> Accept: */*
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Date: Mon, 02 Dec 2024 16:34:18 GMT
< Server: WSGIServer/0.2 CPython/3.12.3
< Content-type: text/plain; charset=utf-8
< Content-Length: 15
<
* transfer closed with 1 bytes remaining to read
* Closing connection
curl: (18) transfer closed with 1 bytes remaining to read
Hello xxxxxxxx%

The solution for this problem would be to peform a shallow copy of the headers list, ideally in the wsgi.headers.Headers class so that it does not modify the passed reference but its own copy.

CPython versions tested on:

3.12

Operating systems tested on:

Linux

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    docsDocumentation in the Doc dir

    Projects

    Status

    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions