DEV Community

John Wick
John Wick

Posted on

Serving Multiple Endpoints from One Bot with StayPresent

How to host multiple flask style endpoints for different bots or status pages using StayPresent's path parameter, get_all, and remove.

Serving Multiple Endpoints from One Bot with StayPresent

A single root route is fine for a simple keep-alive check, but as soon as you're running more than one bot, or want a separate status page from your machine-readable health payload, you need multiple endpoints on the same server. StayPresent supports this natively through a path argument on every response function, without needing to reach for raw Flask routing.

Table of Contents

  1. Why One Route Isn't Enough
  2. Registering Responses at Custom Paths
  3. Path Normalization Rules
  4. Trailing Slashes and Static Assets
  5. Inspecting Every Registered Path
  6. Removing a Registered Path
  7. What Happens When Paths Collide
  8. Full Example: Per-Bot Status Pages
  9. Best Practices
  10. Common Mistakes
  11. FAQs
  12. Conclusion

Why One Route Isn't Enough

If you're running two bots via staypresent.run(bots=[...]), a single JSON blob at / quickly becomes cramped — you end up manually merging both bots' status into one payload. It's cleaner to give each bot its own endpoint, and keep / as a simple top-level summary or the default keep-alive response.

Registering Responses at Custom Paths

text(), json(), html(), and markdown() all accept an optional path argument, defaulting to "/":

import staypresent

staypresent.web.json({"status": "online"}, path="/")
staypresent.web.text("bot #2 is alive", path="/bot2")
staypresent.web.html("dashboard.html", path="/dashboard")
staypresent.web.markdown("CHANGELOG.md", path="/changelog")
Enter fullscreen mode Exit fullscreen mode

Each of these is served completely independently — a request to /bot2 has no relation to whatever is configured at /.

Path Normalization Rules

Paths are normalized automatically to avoid subtle, silent bugs:

  • Surrounding whitespace is stripped — " /status" and "/status " (easy to introduce via an f-string) are treated the same as "/status", rather than silently registering a route nothing will ever match.
  • The path must start with /.
  • Repeated slashes are collapsed.
  • A trailing slash is stripped, so "/status/" and "/status" refer to the same route.

Paths must not contain ? or # — those belong in a query string or fragment, not a route, and StayPresent raises ValueError if they appear. path itself must be a non-empty string; anything else raises TypeError, and an empty or whitespace-only string raises ValueError.

Trailing Slashes and Static Assets

For html()/markdown() registered at any path other than "/", a bare request to that path is automatically redirected (HTTP 308) to a trailing-slash version. A request to /dashboard redirects to /dashboard/. This is what lets relative asset links inside the file — href="style.css", src="images/logo.png" — resolve against that file's own directory instead of its parent. text()/json() responses don't need this since they don't reference any static assets.

Static asset lookups themselves use the longest matching path prefix across every registered html()/markdown() route, so overlapping mounts resolve unambiguously — a request for /dashboard/style.css is served from dashboard.html's own directory, never mistakenly from the root page's directory.

Inspecting Every Registered Path

Three helpers let you introspect what's currently configured, which is useful both for debugging and for building your own tooling on top:

staypresent.web.paths()
# ['/', '/bot2', '/changelog', '/dashboard']

staypresent.web.get_all()
# {'/': {'type': 'json', 'value': {...}}, '/bot2': {...}, ...}

staypresent.web.get("/bot2")
# {'type': 'text', 'value': 'bot #2 is alive'}
Enter fullscreen mode Exit fullscreen mode

Both get() and get_all() return an independent deep copy of the current state — mutating the returned dictionary has no effect on what's actually being served. To change the live response, call text()/json()/html()/markdown() again.

Removing a Registered Path

staypresent.web.remove("/bot2")
# True  (it was registered and is now removed)

staypresent.web.remove("/bot2")
# False (nothing was registered there)
Enter fullscreen mode Exit fullscreen mode

This is useful for a bot that's been shut down or decommissioned but whose status endpoint you don't want to keep serving stale data at.

What Happens When Paths Collide

Registering the same path twice is completely normal and silent — it's exactly how you update a live response, like calling json() again with fresh data to refresh a status endpoint:

staypresent.web.json({"status": "starting"})
# ... later ...
staypresent.web.json({"status": "running"})   # no warning, this is expected usage
Enter fullscreen mode Exit fullscreen mode

What does get logged is the response type at a path changing — e.g. a path held "json" and a later call registers "text" there instead. That's a much stronger signal that two different call sites weren't aware they were both claiming the same path:

staypresent.web.json({"status": "online"}, path="/status")
# elsewhere in the code...
staypresent.web.text("Online", path="/status")
# WARNING logged: path '/status' changed from 'json' to 'text'
Enter fullscreen mode Exit fullscreen mode

Nothing is blocked either way — the newest registration always wins — this only affects what gets logged.

Full Example: Per-Bot Status Pages

import staypresent

staypresent.web.json({"status": "running", "bots": ["telegram", "discord"]})
staypresent.web.json({"status": "online", "queue_depth": 0}, path="/telegram")
staypresent.web.json({"status": "online", "shard": 0}, path="/discord")

staypresent.run(bots=[
    {"file": "telegram_bot.py"},
    {"file": "discord_bot.py"},
])
Enter fullscreen mode Exit fullscreen mode

Each bot's own status can then be checked independently — /telegram, /discord, and the summary at / — all from a single deployment and port.

Best Practices

  • Reserve / for either the default keep-alive response or a top-level summary, and give each bot its own dedicated path.
  • Use web.paths() during development to sanity-check exactly what's currently registered before deploying.
  • If a bot shuts down permanently, call web.remove() on its path rather than leaving a stale status response behind.

Common Mistakes

  • Accidentally reusing a path between two unrelated bots without noticing — watch for the WARNING log line about a response-type change, since that's the signal something's colliding.
  • Forgetting the trailing-slash redirect for html()/markdown() and hardcoding a bare path (without the slash) as a link target elsewhere, causing an unnecessary extra redirect hop.
  • Building a path with untrimmed whitespace from user input or an f-string and assuming it silently fails — it doesn't; normalization handles it, but it's worth understanding why " /status" and "/status" behave identically.

FAQs

Can I register a path other than /health for health-style checks?
Yes — any path is fair game; /health is only special in that it has a built-in default when nothing else is registered there.

Does removing a path affect other registered paths?
No — web.remove() only affects the specific path you pass; everything else stays exactly as configured.

Is there a limit to how many paths I can register?
No documented limit — register as many independent responses as your deployment needs.

Conclusion

Serving multiple endpoints from one bot — or from several bots sharing one deployment — doesn't require reaching for raw Flask routes. StayPresent's path argument, combined with get_all(), paths(), and remove(), gives you a small but complete routing layer without leaving the staypresent.web module.

pip install staypresent[prod]
Enter fullscreen mode Exit fullscreen mode

Top comments (0)