# HTTP Applications

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

Source: https://flasgo.dev/guides/http-apps/

## Routing

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

```python
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](/guides/blueprints-and-url-generation/). Use [Policy and Deployment Checks](/guides/policy-and-deployment-checks/) to review explicit access declarations; `public=True` records intent without bypassing security controls.

### Route precedence and conflicts

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.

## Cross-origin browser APIs

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

```python
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`.

## Requests and globals

Name a parameter `request` to have the current `Request` injected:

```python
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:

```python
@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:

```python
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.

## Forms and uploads

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

```python
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](/guides/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.

## Responses and redirects

For explicit responses, use the class helpers:

```python
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](/guides/response-contracts/) and [Streaming Responses](/guides/streaming-responses/). Ordinary return annotations do not enable runtime response-model enforcement.

## Middleware and error handling

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:

```python
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.

## Sessions and CSRF

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

```python
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](/guides/shared-storage/#revocable-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.

### Session changes and failures

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.

## Templates

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

```python
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](/guides/blueprints-and-url-generation/#generate-urls).

## Static files

Register a directory at construction or later:

```python
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`.

## Built-in OpenAPI and Swagger UI

Docs endpoints are disabled by default:

```python
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:

```python
spec = app.openapi_spec()
```

For CI and deployment, prefer `flasgo openapi` and `flasgo check`; see [CLI and migrations](/guides/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](/reference/api-documentation/).
