mirror of
https://github.com/kennethreitz/responder.git
synced 2026-07-21 18:39:29 +00:00
v4.1.0
Adversarial multi-agent review of the full diff surfaced real issues (several in my own additions); all fixed and regression-tested: - response_model: only validate dict/model bodies and only for actual Pydantic models — `response_model=list` (a documented marker) no longer 500s, and list bodies pass through untouched as in v4.0. - Open-redirect guard: normalize backslashes and leading control/whitespace before classifying, so `/\evil.com` & friends are blocked (browsers treat `\` as `/`). - Server sessions: reverted the dirty-tracking/deepcopy optimization — it broke sliding backend TTL (premature logout) and crashed on non-deepcopyable MemoryBackend values. Back to v4.0 always-write semantics. - Reverted the ServerErrorMiddleware-outermost reorder: it dropped X-Request-ID and mislogged status on 500s. Restored v4.0 ordering. - req.params: decode the scope query string as UTF-8 (not latin-1) so raw non-ASCII queries aren't mojibaked; keeps the no-urlparse speedup. - GraphQL max_depth: follow fragment spreads so depth can't be smuggled via chained fragments (cycles broken; operations measured as roots). - Type-hint body injection: gate to POST/PUT/PATCH/DELETE and skip params that declare a default, so GET and optional-model handlers keep working. Bumps version to 4.1.0 and finalizes the changelog. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Responder
A familiar HTTP Service Framework for Python, powered by Starlette.
import responder
api = responder.API()
@api.route("/{greeting}")
async def greet_world(req, resp, *, greeting):
resp.text = f"{greeting}, world!"
if __name__ == "__main__":
api.run()
$ pip install responder
That's it. Supports Python 3.10+.
The Basics
resp.textsends back text.resp.htmlsends back HTML.resp.contentsends back bytes.resp.mediasends back JSON (or YAML, with content negotiation).resp.file("path.pdf")serves a file with automatic content-type detection.req.headersis case-insensitive.req.paramsgives you query parameters.- Both sync and async views work — the
asyncis optional.
Highlights
# Type-safe route parameters
@api.route("/users/{user_id:int}")
async def get_user(req, resp, *, user_id):
resp.media = {"id": user_id}
# HTTP method filtering
@api.route("/items", methods=["POST"])
async def create_item(req, resp):
data = await req.media()
resp.media = {"created": data}
# Class-based views
@api.route("/things/{id}")
class ThingResource:
def on_get(self, req, resp, *, id):
resp.media = {"id": id}
def on_post(self, req, resp, *, id):
resp.text = "created"
# Before-request hooks (auth, rate limiting, etc.)
@api.route(before_request=True)
def check_auth(req, resp):
if not req.headers.get("Authorization"):
resp.status_code = 401
resp.media = {"error": "unauthorized"}
# Custom error handling
@api.exception_handler(ValueError)
async def handle_error(req, resp, exc):
resp.status_code = 400
resp.media = {"error": str(exc)}
# Lifespan events
from contextlib import asynccontextmanager
@asynccontextmanager
async def lifespan(app):
print("starting up")
yield
print("shutting down")
api = responder.API(lifespan=lifespan)
# GraphQL
import graphene
api.graphql("/graphql", schema=graphene.Schema(query=Query))
# WebSockets
@api.route("/ws", websocket=True)
async def websocket(ws):
await ws.accept()
while True:
name = await ws.receive_text()
await ws.send_text(f"Hello {name}!")
# Mount WSGI/ASGI apps
from flask import Flask
flask_app = Flask(__name__)
api.mount("/flask", flask_app)
# Background tasks
@api.route("/work")
def do_work(req, resp):
@api.background.task
def process():
import time; time.sleep(10)
process()
resp.media = {"status": "processing"}
Built-in OpenAPI docs, cookie-based sessions, gzip compression, static file serving, Jinja2 templates, and a production uvicorn server.
Route convertors: str, int, float, uuid, path.
Documentation
Description
Languages
Python
99.7%
HTML
0.2%