Skip to content

fix(pair): specific error detail for expired vs invalid pairing code#164

Merged
max-tet merged 1 commit into
mainfrom
fix/clayde/pairing-error-detail
Jul 17, 2026
Merged

fix(pair): specific error detail for expired vs invalid pairing code#164
max-tet merged 1 commit into
mainfrom
fix/clayde/pairing-error-detail

Conversation

@ClaydeCode

@ClaydeCode ClaydeCode commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Closes #156 (backend half — the frontend half is FreeshardBase/web-terminal#38).

Problem

POST /public/pair/terminal collapsed every pairing failure into a bare HTTPException(401) with no detail, so FastAPI emitted a generic {"detail": "Unauthorized"} and the web terminal could only render AxiosError: Received HTTP status 401. The service layer already distinguishes InvalidPairingCode from PairingCodeExpired; that information was thrown away at the API boundary.

Change

Split the handler and attach a fixed detail per case, both still 401:

  • PairingCodeExpired → "This pairing code has expired. Generate a new code on your shard and try again."
  • InvalidPairingCode → "This pairing code is not valid."

InvalidPairingCode deliberately covers both "no code issued yet" and "code does not match". Distinguishing them would be an oracle for whether a code is currently outstanding.

The detail strings are hard-coded on purpose

The service-layer exception messages embed the valid pairing code (f"code ({incoming_code}) does not match existing code ({existing_pairing_code.code})"). This is an unauthenticated public endpoint, so passing str(e) through would hand the valid code to any caller who submits a wrong one. log.info(e) keeps the verbose form in the server log, where it belongs.

Also dropped KeyError from the except tuple — it was unreachable, since redeem_pairing_code catches it from database.get_value and re-raises as InvalidPairingCode.

Tests

test_pairing_expired_code passed the whole pairing-code dict instead of its code field. The dict stringified into the query, so the test exercised the invalid-code path and never covered expiry at all. Fixed, and it now asserts the expiry-specific detail.

Added test_pairing_wrong_code_does_not_leak_valid_code: submits a wrong code while a valid one is outstanding and asserts the valid code appears nowhere in the raw response body. This is the check that catches the leak — a correct-code test cannot fail this way.

Verified the tests have teeth by reverting the handler and re-running: 3 of 4 fail without the fix.

4 passed, 12 deselected, 1 warning in 18.51s

Server log during the run confirms the verbose message survives:
INFO shard_core.web.public.pair:pair.py:29 issued code (110799) is expired

Recommended reading order

  1. shard_core/web/public/pair.py — the fix
  2. tests/test_terminals.py — detail assertions + the leak guard

No agents.md update: no architecture, config, or convention changed.

🤖 Generated with Claude Code

The /public/pair/terminal handler collapsed InvalidPairingCode and
PairingCodeExpired into a bare HTTPException(401), so FastAPI emitted a
generic {"detail": "Unauthorized"} and the web terminal could only render
"AxiosError: Received HTTP status 401". The service layer already
distinguishes the two cases; that information was discarded at the API
boundary.

Split the handler and attach a fixed, hand-written detail per case. The
detail strings are hard-coded rather than derived from str(e): the
service-layer messages embed the valid pairing code, and this is an
unauthenticated public endpoint, so forwarding them would hand the valid
code to any caller submitting a wrong one. log.info(e) keeps the verbose
form in the server log.

Both cases stay 401. InvalidPairingCode deliberately covers both "no code
issued yet" and "code does not match" -- telling them apart would be an
oracle for whether a code is currently outstanding.

Drop KeyError from the except tuple: redeem_pairing_code catches KeyError
from database.get_value internally and re-raises it as InvalidPairingCode,
so it was unreachable.

Fix test_pairing_expired_code, which passed the whole pairing-code dict
instead of its "code" field. The dict stringified into the query, so the
test exercised the invalid-code path and never covered expiry at all.
@ClaydeCode
ClaydeCode requested a review from max-tet July 15, 2026 00:34
@max-tet
max-tet merged commit 4090f3a into main Jul 17, 2026
7 checks passed
@max-tet
max-tet deleted the fix/clayde/pairing-error-detail branch July 17, 2026 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

More verbose error message when pairing code is expired

2 participants