Skip to content
Guide

HTTP Applications

Routes, CORS, requests, responses, middleware, sessions, sync and async templates, forms, uploads, and static files.

Use the method decorators or route() for multiple methods:

from flasgo import Flasgo
app = Flasgo()
@app.get("/users/<int:user_id>", name="user-detail")
async def user_detail(user_id: int):
return {"user_id": user_id}
@app.route("/health", methods={"GET", "POST"})
def health():
return {"ok": True}

Available converters are <name> (also written <str:name>), <int:name>, <float:name>, and <path:name>. A GET route also handles HEAD. The convenience decorators cover get, post, put, patch, and delete; add_route() is available for programmatic registration. Pass methods as a collection such as ["POST"]; a bare string such as methods="POST" raises TypeError.

An untyped <name> segment matches any text and is then validated against the handler annotation, so item_id: int on /items/<item_id> accepts /items/5 and returns 422 for /items/five. Use <int:name> when non-numeric paths should not match the route at all.

Flasgo 0.9.0 adds public=, response_model=, and route-level dependencies=. It also supports Blueprints and URL Generation. Use Policy and Deployment Checks to review explicit access declarations; public=True records intent without bypassing security controls.

In 0.9.1, HTTP and WebSocket routes are ordered by specificity rather than registration order. A literal route such as /admin takes precedence over /<path:value>, including when the literal route requires authorization.

HTTP registration raises ValueError for these conflicts:

  • The same path shape has overlapping methods, even if parameter names or converters differ. For example, GET /items/<int:value> and GET /items/<float:value> cannot coexist.
  • The same path shape uses different parameter names across methods. GET /items/<value> and POST /items/<value> are valid; changing the POST parameter to <other> is rejected.
  • Two patterns have equal specificity and can match the same path for a shared method, such as /<value>/bar and /foo/<value> matching /foo/bar.

GET automatically includes HEAD, so a separate HEAD registration for the same shape conflicts. Each route may contain at most one greedy path converter; a literal suffix after it is supported, as in /archive/<path:directory>/download. Resolve conflicts by making paths distinct or combining methods under a consistent parameter contract.

Several converters may share one segment, as in /<a>-<b>-<c>-<d>/end. Since 0.11.0, such routes match in time proportional to the path length times the route length, so long non-matching paths cannot stall the event loop before authentication or rate limiting run.

Registration also raises ValueError for a repeated parameter name such as /<a>/<int:a>, an unknown converter, a malformed placeholder such as <int: id> or <id, and an explicit name= already used by another HTTP or WebSocket route.

CORS is disabled by default. Configure an application policy when JavaScript served from another exact origin needs to read your HTTP route responses:

from flasgo import CORSConfig, Flasgo
browser_api = CORSConfig(
allow_origins={"https://app.example.com"},
allow_methods={"GET", "POST"},
allow_headers={
"authorization",
"content-type",
"x-csrf-token",
},
expose_headers={"x-request-id"},
max_age=600,
)
app = Flasgo(cors=browser_api)
@app.get("/health", cors=False)
def health():
return {"status": "ok"}
@app.post("/partner", cors=CORSConfig(
allow_origins={"https://partner.example.com"},
allow_methods={"POST"},
))
def partner():
return {"accepted": True}

The application policy applies only to registered HTTP routes; framework-owned docs and metrics endpoints do not inherit it. A route can supply a narrower CORSConfig or set cors=False. Policies require exact ASCII http:// or https:// origins without paths. A lone "*" origin is allowed only without credentials, while methods and header names must always be explicit.

Valid preflights return a route-aware 204 without running middleware, authentication, dependencies, or the endpoint, and without creating framework cookies. Ordinary validation, authorization, CSRF, and handled error responses receive CORS headers only for an allowed origin and method. CORS controls browser response access; it is not authentication, authorization, or CSRF protection, and allow_origins does not modify CSRF_TRUSTED_ORIGINS.

Name a parameter request to have the current Request injected:

from flasgo import Request
@app.get("/search")
async def search(request: Request):
return {
"query": request.query_params.get("q", []),
"client": request.client_ip,
"request_id": request.request_id,
}

Request bodies are read asynchronously:

@app.post("/events")
async def event(request: Request):
payload = await request.json()
return {"received": payload}

Flasgo also provides Flask-style context globals for compact handlers:

from flasgo import jsonify, request
@app.get("/inspect")
def inspect():
return jsonify({"method": request.method, "path": request.path})

Prefer explicit injection in reusable code because its dependencies remain visible.

await request.form() parses URL-encoded and multipart bodies into FormData:

from flasgo import Request
@app.post("/profiles")
async def create_profile(request: Request):
form = await request.form()
avatar = form.file("avatar")
return {
"display_name": form.get("display_name"),
"interests": form.getlist("interests"),
"avatar": avatar.filename if avatar else None,
"avatar_bytes": avatar.size if avatar else 0,
}

For typed JSON bodies, query values, headers, cookies, validated forms, and reusable providers, use the Annotated binding model in Request Data and Dependencies. The lower-level Request methods remain useful when an endpoint deliberately needs manual parsing.

Use get, getlist, file, and filelist to avoid collapsing repeated values. Uploaded files are held in memory, so retain the default request-body limit or set a deliberate, bounded MAX_REQUEST_BODY_BYTES. Flasgo also caps multipart parts with MAX_MULTIPART_PARTS and query/form text fields with MAX_FORM_FIELDS; both default to 1000 and return 413 on overflow. Client filenames are sanitized at parse time, but applications should still generate their own storage names.

For explicit responses, use the class helpers:

from flasgo import Response, is_safe_redirect_target, redirect
@app.get("/report")
def report():
return Response.json(
{"ready": True},
status_code=200,
headers={"cache-control": "private, max-age=60"},
)
@app.post("/complete")
def complete(next_page: str = "/done"):
target = next_page if is_safe_redirect_target(next_page) else "/"
response = redirect(target, status_code=303)
response.set_cookie(
"completed",
"yes",
secure=True,
http_only=True,
same_site="Lax",
)
return response

Response header names are case-insensitive: response.headers["Content-Type"] = ... replaces the existing content-type entry instead of sending a second header, and assigning response.content_type updates the header that is sent. Header names, values, and cookies are validated before sending to prevent response splitting; header values reject control characters other than horizontal tab. Use Response.set_cookie() and Response.delete_cookie() instead of constructing raw Set-Cookie values. Cookie names must be non-empty ASCII HTTP tokens. Cookie values and raw Set-Cookie headers reject every C0 control character and DEL. same_site accepts only Lax, Strict, or None; None requires secure=True.

redirect() and Response.redirect() validate header safety but do not decide whether a destination is trusted. Check request-controlled targets with is_safe_redirect_target() first; it accepts relative same-origin locations and rejects absolute, scheme-relative, malformed, control-character, and browser-normalized backslash authority forms. Responses are no-store by default unless you explicitly opt into public caching.

Tuple returns such as (body, status) and (body, status, headers) receive the same status and header validation as explicit responses. 1xx, 204, and 304 responses must have an empty body: constructing one with content raises ValueError, and they are sent without Content-Type or Content-Length. Returning None produces an empty 204.

To validate outgoing fields or send async iterables, see Response Contracts and Streaming Responses. Ordinary return annotations do not enable runtime response-model enforcement.

Before middleware can return a response early. After middleware receives the response from the endpoint, a before-request short-circuit, the built-in 404 and 405 responses, rate-limit rejections, and authentication or authorization denials:

from flasgo import Request, Response, abort
@app.before_request
async def require_tenant(request: Request):
if request.path.startswith("/tenant/") and "x-tenant" not in request.headers:
return Response.json({"error": "tenant required"}, status_code=400)
@app.after_request
def add_service_header(request: Request, response: Response):
response.headers["x-service"] = "accounts"
return response
@app.get("/private")
def private():
abort(404, "Not found")
@app.errorhandler(ValueError)
def invalid_value(request: Request, error: ValueError):
return {"error": "Invalid value"}, 400

Middleware and error handlers may be synchronous or asynchronous. Responses built from exceptions, including abort(), validation errors, and custom error handlers, bypass after_request, as do CSRF and host rejections. In 0.10.0, review after_request hooks that assumed they only saw matched, authorized requests.

abort() raises HTTPException and is annotated -> Never, so type checkers treat the following code as unreachable. str(exc) returns the detail, or the status code when the detail is empty.

With DEBUG=True, unhandled exceptions render a detailed local debugging page. Keep debug mode off in every shared or production environment so stack traces and source context are not exposed.

By default, session data is signed and stored in an HttpOnly cookie:

from flasgo import session
@app.get("/preferences")
def preferences():
session["theme"] = "dark"
return {"theme": session.get("theme")}

Session is a full mutable mapping: membership tests, iteration, len(), del session[key], update(), and setdefault() work, and changes mark the session modified. session.pop(key, None) marks it modified only when the key existed, so reading an absent flash message does not re-issue the session and CSRF cookies. The session, request, and current_user globals forward attribute writes to the request-bound object, so session.modified = True affects only the current request.

Signed cookies prevent undetected modification; they are not encrypted. Do not store passwords, tokens, or other secrets in the session.

For browser-facing unsafe requests, Flasgo uses an HMAC-signed, session-bound double-submit token plus Origin or Referer validation. Read the flasgo-csrf cookie and send the same value in x-csrf-token. Tokens rotate when signed session state changes. Do not disable CSRF merely to make a browser request pass; configure exact trusted origins when cross-origin requests are intentional.

Flasgo 0.9.0 also supports server-side sessions, opaque identifiers, rotation, and revocation. Switching backends requires users with existing signed cookies to log in again. CSRF remains bound to the actual session cookie.

In 0.9.1, an exception during dispatch or function-scoped dependency cleanup restores a deep copy of the loaded session before error handling. Mutations from the failed path are not saved. A custom error handler that returns normally can deliberately save changes to the restored session, such as clearing it for logout. A handler that returns an explicit error response, such as 400, also retains normal session persistence; the status code alone does not trigger rollback.

Response validation runs before session persistence, so malformed headers or cookies cannot commit session changes. This does not roll back database writes or other application side effects, and a transport failure after persistence does not undo a session save.

Duplicate session-cookie occurrences, within one Cookie header or across several, are treated as an absent credential for both signed and server-side sessions. Unsafe requests fail CSRF validation when the CSRF cookie or token header is repeated; origin checking also rejects repeated Origin or Referer headers. Request.cookies reads every Cookie header, and the first value wins for a repeated name. Use Request.cookie_values(name) and Request.header_values(name) when application code must inspect repeated wire values.

Flasgo’s Jinja environment is sandboxed, autoescaped, strict about undefined values, and prevents loader escapes:

from flasgo import Flasgo, Response
app = Flasgo()
app.configure_templates(
"templates",
filters={"uppercase": str.upper},
)
@app.get("/")
def home():
return Response.template(
"home.html",
templates=app.templates,
context={"title": "Welcome"},
)

If you only need the rendered string, use app.render_template("home.html", context).

For templates configured with enable_async=True, render inside handlers with await app.render_template_async(...) or await app.templates.render_async(...), then wrap the string with Response.html(). The synchronous helpers, including Response.template(), raise RuntimeError for async templates inside a running event loop; outside one, such as in a script, synchronous rendering still works. configure_templates() also supplies the request-aware url_for helper; see URL Generation.

Register a directory at construction or later:

app = Flasgo(static_folder="static")
app.configure_static("assets", url_path="/assets", cache_max_age=86_400)

Use url_path="/" to serve the directory at the site root; before 0.11.0 this registered an unmatched //<path:filename> route. Static serving blocks dotfiles, path traversal, and symlink escapes. It supports ETag, Last-Modified, and conditional requests; If-None-Match uses weak comparison and accepts lists and *. Path resolution and stat() run off the event loop. GET bodies stream in bounded chunks without blocking the event loop and derive length and validators from the opened file descriptor; HEAD does not open or read the file. Publicly cacheable static responses do not emit framework-managed CSRF or session cookies.

The content type comes from the requested name rather than a symlink target. Precompressed assets such as app.js.gz keep Content-Encoding: gzip, while archives such as .tar.gz, .tar.bz2, and .xz are served as downloads with their archive type and no Content-Encoding.

Docs endpoints are disabled by default:

app = Flasgo(
settings={
"ENABLE_DOCS": True,
"DOCS_PATH": "/api-docs",
"OPENAPI_PATH": "/api/openapi.json",
"API_TITLE": "Accounts API",
"API_VERSION": "1.0.0",
"API_SERVERS": ["https://api.example.com"],
}
)

You can generate the same specification without exposing an endpoint:

spec = app.openapi_spec()

For CI and deployment, prefer flasgo openapi and flasgo check; see CLI and migrations.

Flasgo generates OpenAPI 3.2 from the same typed endpoint plans used at runtime. Protect both built-in endpoints with DOCS_AUTH_BACKEND whenever they are reachable outside a trusted development environment; see API Documentation.