feat: enhance error handling and configuration validation
Some checks failed
CI / lint-and-test (push) Failing after 27s

- Added a global exception handler to log unhandled exceptions and return a generic 500 JSON response without exposing details to the client.
- Updated the configuration to validate the `DATABASE_URL` format, ensuring it starts with `sqlite://` or `postgresql://`, and log warnings for invalid formats.
- Introduced safe parsing for numeric environment variables (`HTTP_PORT`, `INIT_DATA_MAX_AGE_SECONDS`) with defaults on invalid values, including logging warnings for out-of-range values.
- Enhanced the duty schedule parser to enforce limits on the number of schedule rows and the length of full names and duty strings, raising appropriate errors when exceeded.
- Updated internationalization messages to include generic error responses for import failures and parsing issues, improving user experience.
- Added unit tests to verify the new error handling and configuration validation behaviors.
This commit is contained in:
2026-03-02 23:36:03 +03:00
parent 43386b15fa
commit 7ffa727832
20 changed files with 451 additions and 70 deletions

View File

@@ -3,7 +3,9 @@
import asyncio
import json
import logging
import sys
import threading
import time
import urllib.request
from telegram.ext import ApplicationBuilder
@@ -13,6 +15,9 @@ from duty_teller.config import require_bot_token
from duty_teller.handlers import group_duty_pin, register_handlers
from duty_teller.utils.http_client import safe_urlopen
# Seconds to wait for HTTP server to bind before health check.
_HTTP_STARTUP_WAIT_SEC = 3
async def _resolve_bot_username(application) -> None:
"""If BOT_USERNAME is not set from env, resolve it via get_me()."""
@@ -69,6 +74,25 @@ def _run_uvicorn(web_app, port: int) -> None:
loop.run_until_complete(server.serve())
def _wait_for_http_ready(port: int) -> bool:
"""Return True if /health responds successfully within _HTTP_STARTUP_WAIT_SEC."""
host = config.HTTP_HOST
if host == "0.0.0.0":
host = "127.0.0.1"
url = f"http://{host}:{port}/health"
deadline = time.monotonic() + _HTTP_STARTUP_WAIT_SEC
while time.monotonic() < deadline:
try:
req = urllib.request.Request(url)
with safe_urlopen(req, timeout=2) as resp:
if resp.status == 200:
return True
except Exception as e:
logger.debug("Health check not ready yet: %s", e)
time.sleep(0.5)
return False
def main() -> None:
"""Build the bot and FastAPI, start uvicorn in a thread, run polling."""
require_bot_token()
@@ -85,16 +109,30 @@ def main() -> None:
from duty_teller.api.app import app as web_app
t = threading.Thread(
target=_run_uvicorn,
args=(web_app, config.HTTP_PORT),
daemon=True,
)
t.start()
if config.MINI_APP_SKIP_AUTH:
logger.warning(
"MINI_APP_SKIP_AUTH is set — API auth disabled (insecure); use only for dev"
)
if config.HTTP_HOST not in ("127.0.0.1", "localhost", ""):
print(
"ERROR: MINI_APP_SKIP_AUTH must not be used in production (non-localhost).",
file=sys.stderr,
)
sys.exit(1)
t = threading.Thread(
target=_run_uvicorn,
args=(web_app, config.HTTP_PORT),
daemon=False,
)
t.start()
if not _wait_for_http_ready(config.HTTP_PORT):
logger.error(
"HTTP server did not become ready on port %s within %s s; check port and permissions.",
config.HTTP_PORT,
_HTTP_STARTUP_WAIT_SEC,
)
sys.exit(1)
logger.info("Bot starting (polling)... HTTP API on port %s", config.HTTP_PORT)
app.run_polling(allowed_updates=["message", "my_chat_member"])