6.8. Returning Responses
Table of Contents
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:
- Status line
Example:
HTTP/1.1 200 OK
It includes: - HTTP version
- Status code, for example
200 - Reason phrase, for example
OK - Headers
Key value pairs that describe the response, for example: Content-Type: text/htmlContent-Length: 123Set-Cookie: session=abc123; HttpOnly- 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/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 type | Description | Typical use |
|---|---|---|
text/plain | Plain text | Simple messages, debugging |
text/html | HTML content | Web pages |
application/json | JSON data | APIs, single page applications |
image/png | PNG image | Serving images |
application/octet-stream | Arbitrary binary data | File downloads |
Example: Plain Text Response (Python WSGI style)
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
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
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:
| Code | Meaning | Typical use example |
|---|---|---|
| 200 | OK | Successful GET or POST |
| 201 | Created | Resource successfully created |
| 204 | No Content | Successful request with no body |
| 301 | Moved Permanently | Permanent redirect |
| 302 | Found | Temporary redirect |
| 400 | Bad Request | Invalid input |
| 401 | Unauthorized | Authentication required |
| 403 | Forbidden | Not allowed even if authenticated |
| 404 | Not Found | Resource not found |
| 500 | Internal Server Error | Unexpected server error |
Example: Returning Success vs Error
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:
- Request:
/hello?name=Alex
Response:200 OKwith JSON body{ "message": "Hello, Alex!" } - Request:
/hello
Response:400 Bad Requestwith JSON body{ "error": "name query parameter is required" }
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:
| Header | Example value | Purpose |
|---|---|---|
Content-Type | application/json; charset=utf-8 | Tells client how to read the body |
Content-Length | 123 | Body size in bytes |
Location | /login or full URL | Used in redirects |
Set-Cookie | session=abc123; HttpOnly; Path=/ | Creates or updates cookies |
Cache-Control | no-store or max-age=3600 | Controls caching |
Content-Disposition | attachment; filename="report.pdf" | Controls file download behavior |
Access-Control-Allow-Origin | * or https://example.com | Related to CORS |
Example: Adding Custom Headers
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:
- Convert Python data structures to JSON text.
- Encode JSON text as bytes.
- Set
Content-Typetoapplication/json.
Example: Returning a List of Items
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:
[
{"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/jsonwhen 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
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:
- Set a 3xx status code, usually
302 Foundor301 Moved Permanently. - Set a
Locationheader to the new URL. - Body is often empty or a short message.
Example: Redirect `"/"` to `"/home"`
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:
- A successful DELETE operation can return
204 No Content. - A redirect often does not need a body.
In that case:
- Use an appropriate status code.
- Set
Content-Length: 0. - Return an empty body.
Example: `204 No Content`
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:
- Image files, for example user avatars
- PDF reports
- CSV exports
A file response has:
Content-Typethat matches the file type.Content-Lengthequal to the file size in bytes.- Optionally
Content-Dispositionheader to suggest download.
Example: Returning an Image File
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)
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:
- On success: return a JSON object with data.
- On error: return a JSON object with an error field and possibly a code.
Example pattern:
- Success:
{
"data": {...},
"meta": {...}
}- Error:
{
"error": {
"message": "Description of what went wrong",
"code": "INVALID_INPUT"
}
}Example: Consistent JSON Wrapper
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:
- A response contains a status line, headers, and an optional body.
- You must choose proper status codes, not always
200. - The Content-Type header must match the body, for example JSON vs HTML vs images.
- You can add headers to control caching, cookies, downloads, redirects, and more.
- JSON is the standard for API responses, and you should return consistent JSON shapes.
- Redirects use 3xx codes and
Locationheader. - Some responses, such as
204 No Content, contain no body.
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
KAHIBARO