Theme
Version
GitHub PyPI Discord
On this page

Event routes

Every event handler has its own URL. Citry's browser code calls it for you, but you can also call it yourself: from a plain HTML form that must work without JavaScript, from htmx, or from other code that reads data with a GET request. To send a file from a handler, see Download a file.

Protect every handler

Anyone can send a request to a handler's URL, with any arguments. Check the current user's permissions in every handler, and load records by id rather than trusting values from the browser. State is signed, so it cannot be forged, but anyone can read it, and fields the browser may change can hold any value.

Set up your web framework's CSRF protection as described in Security.

Post a plain form

A plain HTML form keeps working when JavaScript is off or has not loaded yet. Point its action at the handler's URL. self.events.url() builds the URL while the component renders, and get_event_url() builds it outside a render:

from citry.ext.events import actions

class SignupIn:
    email: str

class Signup(Component):
    citry = citry_app

    class Events:
        def submit(self, data: SignupIn):
            create_account(data.email)
            return actions.Redirect("/welcome")

    def template_data(self, kwargs, slots):
        return {"submit_url": self.events.url("submit")}

    template = """
      <form method="post" c-action="submit_url">
        <input name="email">
        <button type="submit">Sign up</button>
      </form>
    """

The response depends on what the handler returns: HTML for a component, a real HTTP redirect for actions.Redirect, or JSON for data. htmx can post to the same URL and swap in the returned HTML.

Allow GET requests

Allow GET on a handler that only reads data, with the methods option of @event(). Browser code and other servers can then call its URL, and Citry's OpenAPI export describes it:

from citry.ext.events import event

class Stats(Component):
    citry = citry_app

    class Events:
        @event(methods=("GET",))
        def summary(self) -> dict:
            return {
                "users": count_users(),
                "active_today": count_active(),
            }

Build the URL while rendering with self.events.url("summary"), or elsewhere with get_event_url(). A GET handler must not change anything on the server. Its arguments arrive in the query string, which holds strings, booleans, numbers, and non-empty lists of those.

Which HTTP methods a handler accepts

methods accepts GET, HEAD, POST, PUT, PATCH, DELETE, and OPTIONS. Any other method raises ValueError when the class is defined. A request with a method the handler does not accept gets a 405 response that lists the accepted ones.

Citry's browser code cannot call a handler whose first method is HEAD or OPTIONS; it raises an error before sending. Call such a handler from a server-side HTTP client.