Nobody has started this yet — be first.
Business impact
A slow network, a double-click on "Sign Up," or a mobile client's automatic retry-on-timeout can all cause two near-simultaneous POST /api/auth/register requests for the same email to reach the server at almost the same instant. Right now, when that happens, one of the two requests doesn't get a clean "that email is already taken" -- it crashes with an unhandled database error. In production that surfaces as a generic, unhelpful 500 internal_error instead of the 409 email_taken the client actually needs to show the user "looks like you already have an account -- try logging in instead." Worse, every one of these races becomes noise in error tracking / on-call alerting for something that is not actually a system failure -- it's an entirely predictable, expected interaction that the code simply doesn't handle.
Problem
register() checks whether the email is taken, and only then commits a new User row: it raises 409 email_taken if User.query.filter_by(email=email).first() is not None, otherwise it builds the user, sets the password, and calls db.session.commit(). This is a classic time-of-check-to-time-of-use (TOCTOU) race: if two requests for the same email both run the SELECT before either one's INSERT commits, both pass the "not taken" check. The first commit() succeeds. The second commit() violates the database's UNIQUE constraint on users.email and raises sqlalchemy.exc.IntegrityError -- which nothing in register() catches.
Current behavior
One of two near-simultaneous registration requests for the same email crashes with an unhandled sqlalchemy.exc.IntegrityError (UNIQUE constraint failed: users.email) instead of getting a clean 409 -- surfacing to the client as a generic 500 and to on-call as unexplained error-tracking noise.
Expected behavior
Whichever of two concurrent same-email registrations loses the race must still get a clean, correct HTTP response: 409 email_taken -- the exact same response the pre-check already produces for the easier, non-concurrent case -- never an unhandled exception, and never a generic 500.
Steps to reproduce
cd flask/movie_watchlist .venv/bin/python -m pytest practicetickets/test_ticket07_registration_race.py -v
Why this matters
The uniqueness pre-check and the write are two separate round-trips to the database with no locking or transaction isolation tying them together -- there's a real window between them where another request can interleave. This is a genuine race condition, not a hypothetical one: it reproduces reliably (5/5 runs in local testing) once two requests are forced to overlap, because Python's normal thread scheduling gives ample opportunity to interleave between the SELECT and the commit() -- especially since password hashing (scrypt) in between takes real wall-clock time.
Suggested approach
The pre-check (if User.query.filter_by(...).first() is not None) is still worth keeping -- it's the fast path that avoids even attempting a write for the common case, and it keeps the error message specific. What's missing is handling the case where the pre-check says "clear," but the database disagrees by the time the commit actually happens. Look at what db.session.commit() raises when the underlying INSERT violates a UNIQUE constraint (the test's failure message will show you the exact exception type), and how the rest of this codebase -- see app/blueprints/movies/omdb_client.py's cache-insert try/except for a pattern already in the codebase -- reacts to a commit that might fail for a reason outside its control. You'll also need db.session.rollback() before you can safely use the session again after a failed commit.
Acceptance criteria
Verification
cd flask/movie_watchlist && .venv/bin/python -m pytest practicetickets/test_ticket07_registration_race.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
$git checkout -b fix/registration-raceFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/registration-raceSubmit 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 inRun the existing test suite (uses an in-memory SQLite DB, no setup needed):
.venv/bin/python -m pytest -v # 33 passed, 0 failed on a clean checkout
cp .env.example .env
# edit .env: set SECRET_KEY, JWT_SECRET_KEY, POSTGRES_PASSWORD to real values
docker compose up --build
This builds the web image, starts Postgres (db), waits for its healthcheck, then runs flask db upgrade and starts gunicorn -- all with one command, no manual migration step. The API is then available at http://localhost:5000.
Work the tickets in practicetickets/ (TICKET_01 through TICKET_07); each names one pytest test file in the same directory. This directory is outside pytest.ini's testpaths = tests, so a bare pytest run from the project root never picks these up -- they only run when pointed at directly:
cd flask/movie_watchlist
.venv/bin/python -m pytest practicetickets/test_ticket01_movie_list_ordering.py -v # a single ticket
./practicetickets/run_tickets.sh # all 7, clean pass/fail summary
run_tickets.sh also unsets TEST_DATABASE_URL/DATABASE_URL for its own run, so a leftover Postgres URL from a different project in your shell doesn't get picked up instead of the in-memory SQLite DB these tests are written against.
Level 1
Fix a bug
Read existing behaviour, correct it.