KAHIBARO
Discord Login Register

6.8. Returning Responses

Understanding Responses in Web Backends

When a client sends a request, your backend must send something back. That "something" is the response. In this chapter you will see what a response is, what it can contain, and how to build useful responses in a simple Python web backend.

You will not learn a specific framework in depth here, that comes later with FastAPI, but the ideas are the same for any backend.


What Is an HTTP Response?

An HTTP response is the message that the server sends back to the client after handling a request. Every response has three main parts:

  1. Status line
    Example:
    HTTP/1.1 200 OK
    It includes:
    • HTTP version
    • Status code, for example 200
    • Reason phrase, for example OK
  2. Headers
    Key value pairs that describe the response, for example:
    • Content-Type: text/html
    • Content-Length: 123
    • Set-Cookie: session=abc123; HttpOnly
  3. Body (optional)
    The actual content, such as:
    • HTML for a web page
    • JSON for an API
    • Image bytes
    • File contents

Example of a raw HTTP response:

http
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 12
Hello world!

The blank line between headers and body is required. After that blank line, everything is the body.

A valid HTTP response must include:

  • A status line with a status code, for example HTTP/1.1 200 OK
  • Zero or more headers
  • A blank line
  • Optional body data
    If you omit the status line, the response is invalid.

Returning Different Content Types

The Content-Type header tells the client how to interpret the body. Returning the right content type is critical for correct behavior.

Common content types:

Content typeDescriptionTypical use
text/plainPlain textSimple messages, debugging
text/htmlHTML contentWeb pages
application/jsonJSON dataAPIs, single page applications
image/pngPNG imageServing images
application/octet-streamArbitrary binary dataFile downloads

Example: Plain Text Response (Python WSGI style)

python
def app(environ, start_response):
    body = b"Hello from backend"
    status = "200 OK"
    headers = [
        ("Content-Type", "text/plain; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ]
    start_response(status, headers)
    return [body]

Example: HTML Response

python
def app(environ, start_response):
    html = b"""
    <html>
      <head><title>Home</title></head>
      <body>
        <h1>Welcome</h1>
        <p>This is a simple HTML page.</p>
      </body>
    </html>
    """
    status = "200 OK"
    headers = [
        ("Content-Type", "text/html; charset=utf-8"),
        ("Content-Length", str(len(html))),
    ]
    start_response(status, headers)
    return [html]

Example: JSON Response

python
import json
def app(environ, start_response):
    data = {"message": "Hello", "status": "ok"}
    body_str = json.dumps(data)
    body = body_str.encode("utf-8")
    status = "200 OK"
    headers = [
        ("Content-Type", "application/json; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ]
    start_response(status, headers)
    return [body]

Notice how only the Content-Type and body format change. The structure of the response is the same.

Always set the correct Content-Type header for the body you send.
For JSON APIs, use Content-Type: application/json.


Controlling Status Codes

Status codes tell the client if the request was successful or not, and what happened.

Some common status codes you will use when returning responses:

CodeMeaningTypical use example
200OKSuccessful GET or POST
201CreatedResource successfully created
204No ContentSuccessful request with no body
301Moved PermanentlyPermanent redirect
302FoundTemporary redirect
400Bad RequestInvalid input
401UnauthorizedAuthentication required
403ForbiddenNot allowed even if authenticated
404Not FoundResource not found
500Internal Server ErrorUnexpected server error

Example: Returning Success vs Error

python
import json
from urllib.parse import parse_qs
def app(environ, start_response):
    query = parse_qs(environ.get("QUERY_STRING", ""))
    name = query.get("name", [None])[0]
    if not name:
        # Missing "name" parameter
        error = {"error": "name query parameter is required"}
        body = json.dumps(error).encode("utf-8")
        status = "400 Bad Request"
    else:
        # All good
        data = {"message": f"Hello, {name}!"}
        body = json.dumps(data).encode("utf-8")
        status = "200 OK"
    headers = [
        ("Content-Type", "application/json; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ]
    start_response(status, headers)
    return [body]

From the client side:

Do not always return 200 OK.
Choose a status code that matches the outcome of the request, for example:

  • 2xx, success
  • 4xx, client error such as bad input
  • 5xx, server error

Setting Response Headers

Headers give additional information about the response. Some typical response headers you will see and use:

HeaderExample valuePurpose
Content-Typeapplication/json; charset=utf-8Tells client how to read the body
Content-Length123Body size in bytes
Location/login or full URLUsed in redirects
Set-Cookiesession=abc123; HttpOnly; Path=/Creates or updates cookies
Cache-Controlno-store or max-age=3600Controls caching
Content-Dispositionattachment; filename="report.pdf"Controls file download behavior
Access-Control-Allow-Origin* or https://example.comRelated to CORS

Example: Adding Custom Headers

python
def app(environ, start_response):
    body = b"Hello with a custom header"
    status = "200 OK"
    headers = [
        ("Content-Type", "text/plain; charset=utf-8"),
        ("Content-Length", str(len(body))),
        ("X-App-Version", "1.0.0"),  # custom header
    ]
    start_response(status, headers)
    return [body]

You can add any header name that starts with X- for custom headers, for example X-Request-Id. In modern APIs, many custom headers are used without X- as well, such as Request-Id. The key point is that both client and server must agree on the meaning.


JSON Responses for APIs

Modern backends often return JSON. JSON is easy to read by both humans and machines. When returning JSON:

  1. Convert Python data structures to JSON text.
  2. Encode JSON text as bytes.
  3. Set Content-Type to application/json.

Example: Returning a List of Items

python
import json
def app(environ, start_response):
    items = [
        {"id": 1, "name": "Apple", "price": 0.5},
        {"id": 2, "name": "Banana", "price": 0.3},
        {"id": 3, "name": "Orange", "price": 0.8},
    ]
    body = json.dumps(items).encode("utf-8")
    status = "200 OK"
    headers = [
        ("Content-Type", "application/json; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ]
    start_response(status, headers)
    return [body]

Client sees JSON like:

json
[
  {"id": 1, "name": "Apple", "price": 0.5},
  {"id": 2, "name": "Banana", "price": 0.3},
  {"id": 3, "name": "Orange", "price": 0.8}
]

When building APIs, adopt this rule:

  • Rule: Return JSON objects or arrays, not plain text, for structured data.
  • Header: Always set Content-Type: application/json when sending JSON.

HTML Responses for Web Pages

If your backend serves web pages, you will return HTML. Later chapters will show you how to use templates. For now, a simple HTML response is just a string with HTML content.

Example: Simple HTML Page

python
def app(environ, start_response):
    html = """
    <!doctype html>
    <html>
      <head>
        <title>My Page</title>
      </head>
      <body>
        <h1>My First Web Page</h1>
        <p>This page is rendered by Python.</p>
      </body>
    </html>
    """
    body = html.encode("utf-8")
    status = "200 OK"
    headers = [
        ("Content-Type", "text/html; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ]
    start_response(status, headers)
    return [body]

Modern development usually uses a template engine to generate such HTML dynamically from data. That comes in later chapters. For this chapter you only need to see that HTML is simply text with Content-Type: text/html.


Redirect Responses

Sometimes you do not want to return content directly, but instead tell the client to go somewhere else. This is a redirect.

To redirect:

  1. Set a 3xx status code, usually 302 Found or 301 Moved Permanently.
  2. Set a Location header to the new URL.
  3. Body is often empty or a short message.

Example: Redirect `"/"` to `"/home"`

python
def app(environ, start_response):
    path = environ.get("PATH_INFO", "/")
    if path == "/":
        # Redirect to /home
        status = "302 Found"
        headers = [
            ("Location", "/home"),
            ("Content-Length", "0"),
        ]
        start_response(status, headers)
        return [b""]
    elif path == "/home":
        body = b"Welcome to the home page"
        status = "200 OK"
        headers = [
            ("Content-Type", "text/plain; charset=utf-8"),
            ("Content-Length", str(len(body))),
        ]
        start_response(status, headers)
        return [body]
    else:
        body = b"Not found"
        status = "404 Not Found"
        headers = [
            ("Content-Type", "text/plain; charset=utf-8"),
            ("Content-Length", str(len(body))),
        ]
        start_response(status, headers)
        return [body]

The browser will automatically follow the redirect and request /home.


Sending Empty Responses

Sometimes you do not need to send a body. For example:

In that case:

Example: `204 No Content`

python
def app(environ, start_response):
    path = environ.get("PATH_INFO", "/")
    if path == "/delete-all":
        # Imagine that we deleted items here
        status = "204 No Content"
        headers = [("Content-Length", "0")]
        start_response(status, headers)
        return [b""]
    body = b"Use /delete-all to delete items"
    status = "200 OK"
    headers = [
        ("Content-Type", "text/plain; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ]
    start_response(status, headers)
    return [body]

File and Binary Responses

Backends also return files, for example:

A file response has:

Example: Returning an Image File

python
import os
def app(environ, start_response):
    path = environ.get("PATH_INFO", "/")
    if path == "/logo":
        file_path = "static/logo.png"
        if not os.path.exists(file_path):
            body = b"Logo not found"
            status = "404 Not Found"
            headers = [
                ("Content-Type", "text/plain; charset=utf-8"),
                ("Content-Length", str(len(body))),
            ]
            start_response(status, headers)
            return [body]
        with open(file_path, "rb") as f:
            body = f.read()
        status = "200 OK"
        headers = [
            ("Content-Type", "image/png"),
            ("Content-Length", str(len(body))),
        ]
        start_response(status, headers)
        return [body]
    body = b"Try /logo"
    status = "200 OK"
    headers = [
        ("Content-Type", "text/plain; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ]
    start_response(status, headers)
    return [body]

Example: Forcing Download (Attachment)

python
import os
def app(environ, start_response):
    path = environ.get("PATH_INFO", "/")
    if path == "/download-report":
        file_path = "data/report.pdf"
        if not os.path.exists(file_path):
            body = b"Report not found"
            status = "404 Not Found"
            headers = [
                ("Content-Type", "text/plain; charset=utf-8"),
                ("Content-Length", str(len(body))),
            ]
            start_response(status, headers)
            return [body]
        with open(file_path, "rb") as f:
            body = f.read()
        status = "200 OK"
        headers = [
            ("Content-Type", "application/pdf"),
            ("Content-Length", str(len(body))),
            ("Content-Disposition", 'attachment; filename="report.pdf"'),
        ]
        start_response(status, headers)
        return [body]
    body = b"Go to /download-report to download"
    status = "200 OK"
    headers = [
        ("Content-Type", "text/plain; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ]
    start_response(status, headers)
    return [body]

The browser will show a "Save As" dialog when it sees Content-Disposition: attachment.


Consistent Response Structure in APIs

For APIs, consistency helps clients. Many APIs follow patterns such as:

Example pattern:

json
  {
    "data": {...},
    "meta": {...}
  }
json
  {
    "error": {
      "message": "Description of what went wrong",
      "code": "INVALID_INPUT"
    }
  }

Example: Consistent JSON Wrapper

python
import json
def json_response(start_response, data=None, error=None, status="200 OK"):
    if error is not None:
        payload = {"error": error}
    else:
        payload = {"data": data}
    body = json.dumps(payload).encode("utf-8")
    headers = [
        ("Content-Type", "application/json; charset=utf-8"),
        ("Content-Length", str(len(body))),
    ]
    start_response(status, headers)
    return [body]
def app(environ, start_response):
    path = environ.get("PATH_INFO", "/")
    if path == "/user":
        # Imagine this data came from a database
        user = {"id": 1, "name": "Alice"}
        return json_response(start_response, data=user, status="200 OK")
    # Unknown path
    return json_response(
        start_response,
        error={"message": "Not found", "code": "NOT_FOUND"},
        status="404 Not Found",
    )

For JSON APIs, define a consistent response format and use it everywhere.
This makes your backend easier to consume and test.


Summary

In this chapter you learned that:

In later chapters, frameworks such as FastAPI will automate much of this, but understanding these building blocks will help you debug and design better backends.

Views: 10

Comments

Please login to add a comment.

Don't have an account? Register now!