Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Python  Module 16 – Practical Python: text, CLIs, web, automation and security

Serving HTTP: http.server and WSGI basics

Serve a folder and a JSON endpoint with Python's http.server, write a WSGI app that a test can call directly, and see what real servers add.

  • Intermediate
  • 20 minutes
  • Examples run with Python 3.14.8 and Pyodide 314.0.7
  • By MySmartCoPilot
Free core, Pro depth: 7 of 20 minutes free
Free: http.server shares a folder with every device that can reach it · A request handler class answers each request · WSGI splits the web app from the server that runs it · Practise: a results API as a WSGI app. With a pass: 2 in-depth parts and 3 interview answers, about 13 minutes.

What you will learn

  • Run python -m http.server for a folder, bound to 127.0.0.1 only
  • Write a JSON endpoint with BaseHTTPRequestHandler
  • Write a WSGI application and test it by calling it with an environ dict
  • Check a WSGI app with wsgiref.validate, and limit the size of request bodies
  • Explain what middleware, production servers and frameworks add to a WSGI app

Before you start

What you’ll learn · Before you start

What you will learn

  • Run python -m http.server for a folder, bound to 127.0.0.1 only
  • Write a JSON endpoint with BaseHTTPRequestHandler
  • Write a WSGI application and test it by calling it with an environ dict
  • Check a WSGI app with wsgiref.validate, and limit the size of request bodies
  • Explain what middleware, production servers and frameworks add to a WSGI app

Before you start

On this page

python -m http.server turns the folder you are in into a website on port 8000 with one command, and unless you say otherwise, every device on the same Wi-Fi can open it. Python’s built-in servers are made for development: the documentation says plainly that http.server is not recommended for production.

This lesson serves a folder and then a JSON endpoint with http.server, and writes the same endpoint as a A Python web application written to PEP 3333: a callable that takes the request as a dict, environ, and a start_response function, and returns the response body as an iterable of bytes.: a function that takes the request as a dict and returns the body as bytes. WSGI, defined in PEP 3333, is the interface between Python web servers and applications, so an app written to it runs under the standard library’s wsgiref now and under a production server later, and a test can call it without opening a port.

http.server shares a folder with every device that can reach it

The module answers a request for a path with the file at that path, and a request for a folder with its index.html or, when it has none, a listing of it. Step through who can reach it, depending on the address it binds to:

Interactive · step by step Who can reach python -m http.server Step through the same command bound two ways. The laptop runs the server; the phone is another device on the same Wi-Fi.
Who can reach python -m http.server, step 6 of 6. Showing Browser on the laptop, Phone on the same Wi-Fi (connection refused), http.server (port 8000, 127.0.0.1 only), The folder it runs in.127.0.0.1:8000the laptop's addressreads filesBrowser on the laptopPhone on the same Wi-Ficonnection refusedhttp.serverport 8000, 127.0.0.1 onlyThe folder it runs inand every folder belowGET /

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. One command, every interface. python -m http.server serves the folder it runs in, on port 8000. Without --bind it listens on every network interface of the laptop, the Wi-Fi included.
  2. Your browser. The browser on the laptop opens http://127.0.0.1:8000/ and gets a listing of the folder, as you meant.
  3. Someone else's phone. A phone on the same Wi-Fi opens the laptop's address on port 8000 and gets the same listing, with every file in the folder and in the folders below it.
  4. Bound to 127.0.0.1. Run it again as python -m http.server --bind 127.0.0.1. Now it listens only on the loopback interface, which only programs on the laptop itself can reach.
  5. The phone is refused. Nothing listens on the laptop's Wi-Fi address at port 8000 any more, so the phone's connection is refused before any file could be read.
  6. Still yours. The browser on the laptop still gets the listing, so local testing works exactly as before.

By default, the documentation says, the server binds itself to all interfaces, which is why the phone got the listing. --bind 127.0.0.1 keeps it to the laptop and --directory serves another folder than the current one. The same server inside a program, bound to 127.0.0.1 and serving a small site folder:

A folder served on 127.0.0.1, and four requests for it Python · server/serve_folder.py
"""What python -m http.server does, inside a program: serve one folder on 127.0.0.1 and fetch from it."""
import functools
import re
import threading
import urllib.error
import urllib.request
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from pathlib import Path

SITE = Path(__file__).parent / "site"


class QuietHandler(SimpleHTTPRequestHandler):
    def log_message(self, *args):  # the real log line starts with the time, which this page never shows
        pass


handler = functools.partial(QuietHandler, directory=str(SITE))
server = ThreadingHTTPServer(("127.0.0.1", 0), handler)       # 127.0.0.1: only this computer can connect
threading.Thread(target=server.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{server.server_port}"
print("serving", SITE.name, "on", server.server_address[0])

for path in ["/", "/notes.txt", "/timetable.csv", "/missing.txt"]:
    try:
        with urllib.request.urlopen(base + path, timeout=5) as response:
            body = response.read().decode()
            shown = "links: " + ", ".join(re.findall(r'href="([^"]+)"', body)) if path == "/" else body.splitlines()[0]
            print(f"{path:15} {response.status} {response.headers.get_content_type():10} {shown}")
    except urllib.error.HTTPError as e:
        with e:
            print(f"{path:15} {e.code} {e.reason}")

server.shutdown()
server.server_close()

Output

serving site on 127.0.0.1
/               200 text/html  links: notes.txt, timetable.csv
/notes.txt      200 text/plain Practicals: Monday and Thursday, 2 to 5 pm
/timetable.csv  200 text/csv   day,subject,room
/missing.txt    404 File not found

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 serve_folder.py

Every file in site became a URL: the listing linked both files, the CSV came back as text/csv, and the missing file got 404. Two more limits from the documentation matter. The handler follows symbolic links, so a link inside the folder can serve a file outside it. And the --tls-cert and --tls-key options that Python 3.14 added make the connection private without changing who may connect.

Serving your home folder

python -m http.server run in your home folder lists everything under it, your documents and your .ssh folder included, to anyone on the network. Run it in a folder that holds only what you mean to share, and add --bind 127.0.0.1 unless another device really needs it.

Checkpoint

Check: http.server and who can reach it

  1. Question 1 of 2 You run python -m http.server on a laptop connected to a café's Wi-Fi. Who can open the folder listing?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Anyone on that network who can reach the laptop's address

    The documentation says the server binds itself to all interfaces by default. Pass --bind 127.0.0.1 to keep it to the machine it runs on.

  2. Question 2 of 2 The folder you serve contains a symbolic link named data that points to a private folder elsewhere on the disk. What can a visitor get?
    Show the answer: Question 2 of 2

    Answer: The files of the private folder, through /data/

    The documentation warns that the handler follows symbolic links, which makes it possible to serve files outside the folder. Serve a folder that holds only the files you mean to share, with no links in it.

A request handler class answers each request

For anything but files, subclass BaseHTTPRequestHandler. The server creates one handler for each request and calls the method named after the HTTP method, do_GET() for a GET. The handler writes the whole response itself, in order: send_response() for the status line, send_header() for each header, end_headers() for the blank line that ends them, then the body to self.wfile:

A JSON endpoint, three GETs and a POST it does not handle Python · server/json_endpoint.py
"""A JSON endpoint with BaseHTTPRequestHandler: one method per HTTP method, and the response written by hand."""
import json
import threading
import urllib.error
import urllib.parse
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

MARKS = {"21": {"name": "Asha", "maths": 91}, "22": {"name": "Ravi", "maths": 78}}
LOG = []


class MarksHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        url = urllib.parse.urlsplit(self.path)
        roll = urllib.parse.parse_qs(url.query).get("roll", [""])[0]
        if url.path == "/health":
            self.send_json(200, {"status": "ok"})
        elif url.path == "/marks" and roll in MARKS:
            self.send_json(200, MARKS[roll])
        else:
            self.send_json(404, {"error": "not found"})

    def send_json(self, status, data):
        body = json.dumps(data).encode()
        self.send_response(status)                      # the status line, plus Server and Date headers
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()                              # the blank line between headers and body
        self.wfile.write(body)

    def log_message(self, format, *args):              # the default writes each line to stderr after a time stamp
        LOG.append(format % args)


server = ThreadingHTTPServer(("127.0.0.1", 0), MarksHandler)
threading.Thread(target=server.serve_forever, daemon=True).start()
base = f"http://127.0.0.1:{server.server_port}"

for path in ["/health", "/marks?roll=21", "/marks?roll=99"]:
    try:
        with urllib.request.urlopen(base + path, timeout=5) as response:
            print(path, response.status, json.loads(response.read()))
    except urllib.error.HTTPError as e:
        with e:
            print(path, e.code, json.loads(e.read()))
try:
    urllib.request.urlopen(urllib.request.Request(base + "/marks", data=b"x"), timeout=5)
except urllib.error.HTTPError as e:
    with e:
        print("POST /marks", e.code, e.reason)

server.shutdown()
server.server_close()
print("the server's log, without its time stamps:")
for line in LOG:
    print("  ", line)

Output

/health 200 {'status': 'ok'}
/marks?roll=21 200 {'name': 'Asha', 'maths': 91}
/marks?roll=99 404 {'error': 'not found'}
POST /marks 501 Unsupported method ('POST')
the server's log, without its time stamps:
   "GET /health HTTP/1.1" 200 -
   "GET /marks?roll=21 HTTP/1.1" 200 -
   "GET /marks?roll=99 HTTP/1.1" 404 -
   code 501, message Unsupported method ('POST')
   "POST /marks HTTP/1.1" 501 -

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 json_endpoint.py

The POST got 501, unsupported method, because the class has no do_POST(). The log lines went through log_message(), which normally writes them to standard error after a time stamp; overriding it is how a program sends them to its own log. Writing every response by hand is where this approach stops growing well: each route repeats the same four steps, and nothing checks that the headers match the body.

Checkpoint

Check: Request handler classes

  1. Question 1 of 2 In a BaseHTTPRequestHandler, which call comes after the last send_header() and before the body is written?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: end_headers()

    The order is fixed by HTTP: the status line, the headers, a blank line, the body. send_response(), send_header(), end_headers(), then self.wfile.write(body).

  2. Question 2 of 2 MarksHandler defines do_GET() and no do_POST(). What does a POST to it get?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 501, unsupported method

    BaseHTTPRequestHandler looks for a method named do_ plus the HTTP method. When there is none, it answers 501 Unsupported method and logs it.

WSGI splits the web app from the server that runs it

PEP 3333 defines the contract in two calls. The server calls app(environ, start_response), where environ is a dict of the request with keys such as REQUEST_METHOD, PATH_INFO and QUERY_STRING. The app calls start_response(status, headers) with a status string such as '200 OK' and a list of (name, value) pairs, all of them str, and returns an iterable of bytes. Step through one request:

Interactive · message by message One request through a WSGI server and an app Step through GET /marks?roll=22 from served.py. The server turns HTTP into a dict, and the app's return value back into HTTP.
One request through a WSGI server and an app, step 6 of 6. Messages so far: HTTP client to WSGI server: GET /marks?roll=22; WSGI server to app(environ, …): app(environ, start_response); app(environ, …) to WSGI server: start_response('200 OK', …); app(environ, …) to WSGI server: return [body bytes]; WSGI server to HTTP client: 200 OK, JSON body.HTTP clienturllib in served.pyWSGI serverenviron builtapp(environ, …)returnedGET /marks?roll=22app(environ, start_response)start_response('200 OK', …)return [body bytes]200 OK, JSON body

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A request arrives. The client sends GET /marks?roll=22 to the server's port, as any browser or HTTP client would.
  2. HTTP becomes a dict. The server reads the request line and the headers and builds environ, a dict in which REQUEST_METHOD is GET, PATH_INFO is /marks and QUERY_STRING is roll=22.
  3. The server calls the app. It calls the application with environ and a start_response function of its own. The app never sees a socket.
  4. Status and headers. The app finds roll 22, encodes the JSON, and calls start_response() with '200 OK' and a list of header pairs, every name and value a str.
  5. The body, as bytes. It returns a list with one bytes object. The server does not care how the app made it, only that it is an iterable of bytes.
  6. Back to HTTP. The server writes the status line, the headers and the body to the connection. A test can skip the network entirely and call the app with a dict of its own, as call_app.py does.

The WSGI app called directly, as a test calls it

wsgi/call_app.py

"""Calling the WSGI app directly, as a test does: no server, no network, just a dict in and bytes out."""
from wsgiref.util import setup_testing_defaults

from marks_app import app


def call(app, path, query=""):
    environ = {"PATH_INFO": path, "QUERY_STRING": query}
    setup_testing_defaults(environ)                  # fills in REQUEST_METHOD, SERVER_NAME, wsgi.input and the rest
    seen = {}

    def start_response(status, headers, exc_info=None):
        seen["status"], seen["headers"] = status, headers

    body = b"".join(app(environ, start_response))
    return seen["status"], dict(seen["headers"]), body


for path, query in [("/health", ""), ("/marks", "roll=21"), ("/marks", "roll=99")]:
    status, headers, body = call(app, path, query)
    print(f"{path}?{query}".rstrip("?"), "->", status, headers["Content-Type"], body.decode())

Output

/health -> 200 OK application/json {"status": "ok"}
/marks?roll=21 -> 200 OK application/json {"name": "Asha", "maths": 91}
/marks?roll=99 -> 404 Not Found application/json {"error": "not found"}

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 call_app.py

wsgi/marks_app.py

"""A WSGI application: one callable that takes the request as a dict and returns the body as bytes."""
import json
from urllib.parse import parse_qs

MARKS = {"21": {"name": "Asha", "maths": 91}, "22": {"name": "Ravi", "maths": 78}}


def app(environ, start_response):
    path = environ["PATH_INFO"]
    roll = parse_qs(environ.get("QUERY_STRING", "")).get("roll", [""])[0]
    if path == "/health":
        status, data = "200 OK", {"status": "ok"}
    elif path == "/marks" and roll in MARKS:
        status, data = "200 OK", MARKS[roll]
    else:
        status, data = "404 Not Found", {"error": "not found"}
    body = json.dumps(data).encode()
    start_response(status, [("Content-Type", "application/json"), ("Content-Length", str(len(body)))])
    return [body]

call_app.py is a whole test harness: setup_testing_defaults() fills in the keys a server would add, and its start_response only records what the app passes. Because the app is a plain function, these tests take milliseconds, open no port and run in your browser. Press Edit, change the last roll from 99 to 22 and run it again.

Four things stand between this function and a public website:

  • wsgiref: wsgiref.simple_server runs a WSGI app on a real port for local tests, and wsgiref.validate checks that an app follows PEP 3333.
  • Middleware: a WSGI app that wraps another and adds something to every route, such as headers or error pages.
  • Production servers: Gunicorn, Waitress and mod_wsgi run the same app with many workers, timeouts and logs.
  • Frameworks: Flask and Django are WSGI apps that add routing, templates and sessions; FastAPI uses ASGI, the asynchronous interface.

Key idea

A WSGI app is a function of environ and start_response that returns bytes, so a test can call it with a dict, with no server and no port.

Checkpoint

Check: The WSGI contract

  1. Question 1 of 2 For a request to /marks?roll=22, what does the server put in environ['PATH_INFO'] and environ['QUERY_STRING']?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: '/marks' and 'roll=22'

    PEP 3333 takes these keys from CGI: PATH_INFO is the path, QUERY_STRING the part after ?, without the ?.

  2. Question 2 of 2 What must a WSGI application return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: An iterable of bytes, such as [body]

    The status and headers go through start_response(), and the body comes back as an iterable of bytes, usually a list with one item.

Practise: a results API as a WSGI app

Write a WSGI app that answers /health and /results?roll=… with JSON, and the right status for a missing roll, an unknown one and a POST. The sample tests call it with dicts, as call_app.py does, which is why they run in your browser. When they pass, a practice session asks one question at a time, weakest idea first, and explains each answer as you give it.

Exercise · Medium · Python

A results API as a WSGI app

Finish the WSGI application app(environ, start_response) in results_app.py. The tests call it directly with an environ dict, as a server would, so nothing here opens a port.

Every answer is JSON made with json.dumps(data, ensure_ascii=False).encode(), so that names such as अदिति stay readable, and carries two headers: Content-Type: application/json and a Content-Length that counts the bytes of the body.

  • GET /health answers 200 OK with {"status": "ok"}.
  • GET /results?roll=21 answers 200 OK with the entry of RESULTS for that roll.
  • GET /results without a roll answers 400 Bad Request with {"error": "roll is required"}.
  • An unknown roll, or any other path, answers 404 Not Found with {"error": "not found"}.
  • Any method other than GET answers 405 Method Not Allowed with {"error": "use GET"} and the extra header Allow: GET.

Starter code · results_app.py

import json
from urllib.parse import parse_qs

RESULTS = {"21": {"name": "Asha", "total": 452}, "22": {"name": "अदिति", "total": 471}}


def app(environ, start_response):
    """/health, /results?roll=<n>; 400 without a roll, 404 when not found, 405 for methods other than GET."""
    body = json.dumps({"status": "ok"}).encode()
    start_response("200 OK", [("Content-Type", "application/json")])
    return [body]
The sample tests · test_results_app.py
import json
from wsgiref.util import setup_testing_defaults

from results_app import app


def call(path, query="", method="GET"):
    environ = {"PATH_INFO": path, "QUERY_STRING": query, "REQUEST_METHOD": method}
    setup_testing_defaults(environ)
    seen = {}

    def start_response(status, headers, exc_info=None):
        seen["status"], seen["headers"] = status, dict(headers)

    body = b"".join(app(environ, start_response))
    return seen["status"], seen["headers"], body


def test_health():
    """/health answers 200 with JSON and the right length"""
    status, headers, body = call("/health")
    assert status == "200 OK"
    assert headers["Content-Type"] == "application/json"
    assert json.loads(body) == {"status": "ok"}
    assert headers["Content-Length"] == str(len(body))


def test_result_found():
    """a known roll answers 200 with its entry, and Content-Length counts bytes"""
    status, headers, body = call("/results", "roll=22")
    assert status == "200 OK"
    assert json.loads(body) == {"name": "अदिति", "total": 471}
    assert "अदिति".encode() in body
    assert headers["Content-Length"] == str(len(body))


def test_missing_roll():
    """/results without a roll answers 400"""
    status, headers, body = call("/results")
    assert status == "400 Bad Request"
    assert json.loads(body) == {"error": "roll is required"}


def test_not_found():
    """an unknown roll and an unknown path answer 404"""
    for path, query in [("/results", "roll=99"), ("/nowhere", "")]:
        status, headers, body = call(path, query)
        assert status == "404 Not Found"
        assert json.loads(body) == {"error": "not found"}


def test_other_methods():
    """POST answers 405 with an Allow header"""
    status, headers, body = call("/results", "roll=21", method="POST")
    assert status == "405 Method Not Allowed"
    assert headers["Allow"] == "GET"
    assert json.loads(body) == {"error": "use GET"}
A hint

Write one small reply(status, data, extra=()) inside app() that encodes the body, builds the header list and calls start_response(); every branch then ends with return reply(...). Read the roll with parse_qs(environ.get("QUERY_STRING", "")).get("roll", [""])[0].

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Practice that adapts to you

Practise this lesson

9 questions about 8 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 Which option keeps python -m http.server to the machine it runs on?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: --bind 127.0.0.1

    The bind address decides who can connect; 127.0.0.1 is reachable only from the machine itself.

  2. Question 2 of 9 You start python -m http.server with --tls-cert and --tls-key and no --bind. What changes for a device on the same network?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: Its connection is encrypted, and it can still read every file in the folder

    Encryption and access are different things. Keep the folder small and bind to 127.0.0.1 to limit who connects; TLS then only keeps the traffic private.

  3. Question 3 of 9 During a meeting a colleague needs one PDF from your laptop, over the office Wi-Fi. How do you share it with http.server most safely?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Put a copy of the PDF in a new, empty folder, serve only that folder with --directory, and stop the server after the download

    Serve only what you mean to share, for as long as needed. The bind address has to be reachable by the colleague, so the folder's contents are what keep the rest of your disk private.

  4. Question 4 of 9 Which method of a BaseHTTPRequestHandler subclass answers a GET request?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: do_GET()

    The server calls do_GET(), do_POST() and so on, by the method of each request.

  5. Question 5 of 9 Why does send_json() in json_endpoint.py send a Content-Length header?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: So that the client knows where the body ends

    Content-Length marks the end of the body for the client. It has to count the bytes actually written, which is why the endpoint computes it from the encoded body.

  6. Question 6 of 9 Your BaseHTTPRequestHandler has grown to twelve routes, each repeating send_response(), send_header() and end_headers(). What is the better next step?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: Write the app as a WSGI function, or use a framework, and run it on a WSGI server

    http.server is for development. Once an app has real routes, the WSGI interface separates the app from the server, and a framework adds the routing.

  7. Question 7 of 9 What does a WSGI app pass to start_response()?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: A status string such as '200 OK' and a list of (name, value) pairs

    start_response(status, headers) sends the status and headers; the app then returns the body.

  8. Question 8 of 9 Using call() and app from the lesson's call_app.py and marks_app.py, what does this print?

    Read the code, then choose one answer.

    status, headers, body = call(app, "/marks", "roll=99")
    print(status, body.decode())
    Show the answer: Question 8 of 9

    Answer: 404 Not Found {"error": "not found"}

    Called directly, the app returns its status and body like any function. That is what makes WSGI apps quick to test.

  9. Question 9 of 9 Why write an endpoint as a WSGI app rather than as a BaseHTTPRequestHandler?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: The same app runs unchanged on any WSGI server, and tests can call it as a function

    The interface is the point: write the app once to PEP 3333, test it with plain dicts, and run it under wsgiref, Gunicorn or Waitress without changing it.

Running and checking a WSGI app: wsgiref, validation and request bodies Pro: opens with a Pro, Premium or Ultimate pass

A WSGI app that passes its own tests can still break under a real server: a header of the wrong type, a length counted in characters, a request body that never ends. This part serves the app on a real port, reproduces five bugs and their fixes, and reads request bodies within a limit.

In this part:

  • The same app on wsgiref, from socket to environ
  • Five bugs, and what wsgiref.validate says about each
  • Request bodies: the length header, a size limit and the input stream

4 runnable examples (Python) · about 5 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

From a development server to production: middleware, workers and frameworks Pro: opens with a Pro, Premium or Ultimate pass

A production setup wraps the same WSGI app in layers: middleware for what every route needs, a server with workers, and a proxy in front. This part builds a middleware that adds security headers and safe error pages, sizes a worker pool with numbers, and sets out what frameworks and ASGI add.

In this part:

  • Middleware: security headers and a safe 500 around every route
  • Workers, slow clients and a proxy in front, with numbers
  • What frameworks add, and when ASGI fits better

1 diagram · 1 runnable example (Python) · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Interview questions

  1. Fresher to mid · Warm-up

    What does a WSGI server pass to your application, and what must the application return?

    The server calls the application with two arguments. environ is a dict that describes the request: keys such as REQUEST_METHOD, PATH_INFO, QUERY_STRING and CONTENT_LENGTH, one HTTP_ key for each request header (User-Agent arrives as HTTP_USER_AGENT), and wsgi.input, a stream that holds the body. start_response is a function. The application calls start_response(status, headers) once, with a status string such as '200 OK' and a list of (name, value) pairs in which every name and value is a str, and then returns an iterable of bytes, usually a list with one bytes object. Because the application is a plain function, a test can call it with a dict and check the status, the headers and the bytes, with no server, no socket and no port.

  2. Fresher to mid

    A teammate ran python -m http.server in the project folder at a hackathon to show a build to one judge, and left it running all afternoon. The terminal log shows requests from 40 addresses, some for files outside the build. What was exposed, and what do you do now?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. Mid to senior

    Users with Hindi and Tamil names say your WSGI JSON API sometimes returns cut-off answers, while English names always work. The app sets Content-Length itself. Find the cause, fix it, and say which test would have caught it.

    Model answer, Pro. First step and follow-ups

  4. Mid to senior

    A WSGI service on nine sync workers handles 200 requests a second at 40 ms each. When 20 students upload 2 MB photos from slow phones at 50 KB a second, every other page times out while the CPU stays at 10 %. Explain what is happening and how you fix it.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • python -m http.server binds to every interface unless you pass --bind 127.0.0.1, follows symbolic links, and is for development only.
  • A BaseHTTPRequestHandler writes each response by hand: status line, headers, a blank line, then the body.
  • A WSGI app is a function of environ and start_response that returns an iterable of bytes, with every header a str.
  • Because a WSGI app is a plain function, a test can call it with a dict, without a server or a port.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.