Nobody has started this yet — be first.
Business impact
Every single short URL that has an expires_at passes through a window, in its final second of validity, where the very next click 500s instead of either redirecting or cleanly 410ing. For a high-traffic link with a precise expiry (a flash-sale link, a time-boxed event RSVP, a one-time signed invite), this means real users get a server error at exactly the moment the link matters most, and it happens on every expiring link, every time, not as a rare edge case.
Problem
seconds_to_expiry = int((expires_at - datetime.utcnow()).total_seconds()); ttl = min(ttl, seconds_to_expiry); redis_client.setex(_cache_key(slug), ttl, payload). When a link expires in under 1 second, int(...) truncates seconds_to_expiry to 0 (or it is already negative by the time this executes).
Current behavior
redis_client.setex(key, 0, ...) (or a negative TTL) raises redis.exceptions.ResponseError: invalid expire time in setex -- both real Redis and fakeredis reject a non-positive expire time outright. This exception is never caught, so it propagates all the way up through resolve_short_url and crashes the request.
Expected behavior
A link that's still valid -- even by less than a second -- should redirect successfully (302). Whether it gets cached at all in that final sliver of its lifetime is a judgment call, but the request must never crash because of it.
Steps to reproduce
Why this matters
This was a real, deliberate safety guard (max(1, ...)) that got removed -- min(ttl, seconds_to_expiry) alone has no floor, so it happily computes 0 or a negative number and hands it straight to Redis. Redis's SETEX (and EXPIRE/PEXPIRE generally) treats a non-positive expiry as a protocol-level error, not a "just expire it immediately" no-op -- so any code path that can produce ttl <= 0 needs to either floor it at 1, or skip caching entirely for that call.
Suggested approach
Decide whether the fix should floor the TTL at a minimum of 1 second (simplest: still gets a cache entry, just a very short-lived one) or skip the set_cached_url call entirely when the link is about to expire (arguably more correct: no point caching something that is about to become invalid). Either way, redis_client.setex must never be called with ttl <= 0.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_13_cache_ttl_crash.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
Sign in to create it automatically, or do it yourself:
$git checkout -b fix/cache-ttl-crashFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/cache-ttl-crashSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign indocker-entrypoint.shcurl -X POST http://localhost:5000/api/urls \
-H "Content-Type: application/json" \
-d '{"target_url": "https://anthropic.com", "custom_slug": "claude"}'
# -> {"slug": "claude", "short_url": "http://localhost:5000/claude", "expires_at": null}
curl -i http://localhost:5000/claude # 302 redirect, records a click
make seed # or: docker compose exec web flask seed-db
If something is already listening on 5432/6379 locally, uncomment POSTGRES_HOST_PORT / REDIS_HOST_PORT in .env to remap the host side — the app always talks to db/redis over the compose network regardless. Run production-style (gunicorn, no dev server, no bind mount) with docker compose -f docker-compose.prod.yml up --build. GET /healthz checks both Postgres and Redis connectivity.
You do not need Docker running at all to work the tickets — the test suite uses TestingConfig (SQLite in-memory via StaticPool) and fakeredis instead of a real Postgres/Redis, so everything runs against a plain virtualenv:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/pytest practice_tickets/tests/test_ticket_01_base62_roundtrip.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 20, clean pass/fail summary
./practice_tickets/run_tickets.sh 05 13 16 # just a few, by number
All 20 tests fail at the start; as you fix each ticket in app/, its test (and only its test) should flip to passing. .venv/bin/pytest tests/ (via make test) runs the project's own main suite — some ticket bugs (TICKET-05, 07, 08, 09, 14 among others) also break assertions in that suite, since they are real bugs in shared code paths; fixing the ticket fixes those too, not a separate task.
Level 1
Fix a bug
Read existing behaviour, correct it.