You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reject a JSON boolean supplied for client_sent_msg_ts or client_timeout with HTTP 400, instead of silently treating true as 0.001 and false as "field omitted".
Context you need
src/interfaces/chat_app/request_validation.py normalizes both timing fields through _milliseconds_to_seconds, which guards the division against OverflowError and TypeError. bool is a subclass of int in Python, so a boolean satisfies neither guard and sails through:
Measured on dev at dd3fb6b4 — all four combinations are accepted, none raises:
payload
result
consequence
"client_timeout": true
0.001
a 1-millisecond deadline. Any request that also supplies client_sent_msg_ts is past it on arrival, so _prepare_chat_context returns 408; on the streaming route the in-stream check ends the stream with an in-band 408 almost immediately.
"client_timeout": false
0
silently identical to omitting the field
"client_sent_msg_ts": true
0.001
persisted as 1970-01-01T00:00:00.001Z — indistinguishable from a real measurement at a glance, and not the documented 1970-01-01T00:00:00Z absent-value sentinel
"client_sent_msg_ts": false
0
silently identical to omitting the field
true is the harmful one: a client that serializes a flag into the wrong field gets 408 on every request with no indication that the field was the problem.
Why this is not already covered. The guards catch what cannot be divided (OverflowError, TypeError); a boolean divides fine. The falsey guard if not value: return 0 runs first, so False never reaches the division at all — a type check placed after it would miss false entirely. Put the boolean check before the falsey guard.
How it was found. Adversarial review of PR for #194 (docs), which had claimed "anything not numeric is rejected". That claim was corrected to name only strings, arrays and objects — the types actually rejected — so the documentation is currently accurate about a contract that has this hole. Closing the hole lets the docs say "a boolean" too.
Not a regression. This behaviour predates #175/#185; those changes added the guards that made every other non-numeric type a 400 and left this one gap.
Constraints
Branch from origin/dev. PR with gh pr create --repo fasrc/archi --base dev. Notupstream/dev. Never commit to dev directly.
TDD: add the failing endpoint tests first, watch them fail with a 200/408 rather than 400, then change request_validation.py.
bash scripts/gate.sh must pass, run bare (no pipe, no redirect), with ≥80% diff coverage on changed lines. Never --no-verify.
No Co-Authored-By or AI-attribution trailers. Short lowercase commit subject.
Update docs/docs/api_reference.md in the same change — both timing-field rows list the rejected types, and a boolean belongs in that list once it is rejected. Do not renumber any [name]: …/app.py#Lnnn anchor (that is Make the api_reference.md app.py line anchors self-verifying #190).
Uses OpenSpec: this changes a documented request contract, so add a delta to chat-api-request-contract rather than editing a main spec in place.
Never merge. A human merges.
Plan
Add failing cases to tests/unit/test_chat_timing_field_validation.py::TestNormalizationItselfCannotRaise — its CASES list and _post harness already drive both routes with a real JSON body. Add (field, True) and (field, False) for both fields; all four must fail before the fix.
In _milliseconds_to_seconds, reject isinstance(value, bool)before the if not value guard, raising InvalidClientTiming with a message naming the field, consistent with the existing wording.
Confirm no legitimate caller sends booleans: grep -rn "client_timeout\|client_sent_msg_ts" src/interfaces/chat_app/static/ src/interfaces/chat_app/openai_compat.py — the JS client sends Date.now() and a number, and openai_compat.py synthesizes now.timestamp().
Add the OpenSpec delta and add "a boolean" to the rejected-type lists in both api_reference.md rows.
Commands
# reproduce (expect: 0.001 0)
python -c "from src.interfaces.chat_app.request_validation import parse_client_timeout as p; print(p(True), p(False))"# red first, then green
python -m pytest tests/unit/test_chat_timing_field_validation.py -q
bash scripts/gate.sh
openspec validate <change-name> --strict
Acceptance criteria
python -c "from src.interfaces.chat_app.request_validation import parse_client_timeout as p; p(True)" exits non-zero with InvalidClientTiming; same for p(False), parse_client_sent_msg_ts(True), parse_client_sent_msg_ts(False).
Both chat routes return 400 for a boolean in either timing field, and the error names the field.
A real number is still accepted: parse_client_timeout(600000) == 600.0 and parse_client_sent_msg_ts(1700000000000) == 1700000000.0.
An omitted field still yields 0 — parse_client_timeout(None) == 0. Rejecting false must not break omission.
Run the reproduce command above and confirm it prints 0.001 0. Then read _milliseconds_to_seconds and note that the falsey guard precedes the division — that ordering is why the check has to go first.
Found by adversarial review of the #194 docs PR. Related: #190 (anchor drift), #193 (nullable timing column), #194 (the docs rows this would let us strengthen).
Objective
Reject a JSON boolean supplied for
client_sent_msg_tsorclient_timeoutwith HTTP 400, instead of silently treatingtrueas0.001andfalseas "field omitted".Context you need
src/interfaces/chat_app/request_validation.pynormalizes both timing fields through_milliseconds_to_seconds, which guards the division againstOverflowErrorandTypeError.boolis a subclass ofintin Python, so a boolean satisfies neither guard and sails through:Measured on
devatdd3fb6b4— all four combinations are accepted, none raises:"client_timeout": true0.001client_sent_msg_tsis past it on arrival, so_prepare_chat_contextreturns 408; on the streaming route the in-stream check ends the stream with an in-band 408 almost immediately."client_timeout": false0"client_sent_msg_ts": true0.0011970-01-01T00:00:00.001Z— indistinguishable from a real measurement at a glance, and not the documented1970-01-01T00:00:00Zabsent-value sentinel"client_sent_msg_ts": false0trueis the harmful one: a client that serializes a flag into the wrong field gets 408 on every request with no indication that the field was the problem.Why this is not already covered. The guards catch what cannot be divided (
OverflowError,TypeError); a boolean divides fine. The falsey guardif not value: return 0runs first, soFalsenever reaches the division at all — a type check placed after it would missfalseentirely. Put the boolean check before the falsey guard.How it was found. Adversarial review of PR for #194 (docs), which had claimed "anything not numeric is rejected". That claim was corrected to name only strings, arrays and objects — the types actually rejected — so the documentation is currently accurate about a contract that has this hole. Closing the hole lets the docs say "a boolean" too.
Not a regression. This behaviour predates #175/#185; those changes added the guards that made every other non-numeric type a 400 and left this one gap.
Constraints
origin/dev. PR withgh pr create --repo fasrc/archi --base dev. Notupstream/dev. Never commit todevdirectly.request_validation.py.bash scripts/gate.shmust pass, run bare (no pipe, no redirect), with ≥80% diff coverage on changed lines. Never--no-verify.Co-Authored-Byor AI-attribution trailers. Short lowercase commit subject.docs/docs/api_reference.mdin the same change — both timing-field rows list the rejected types, and a boolean belongs in that list once it is rejected. Do not renumber any[name]: …/app.py#Lnnnanchor (that is Make the api_reference.md app.py line anchors self-verifying #190).chat-api-request-contractrather than editing a main spec in place.Plan
tests/unit/test_chat_timing_field_validation.py::TestNormalizationItselfCannotRaise— itsCASESlist and_postharness already drive both routes with a real JSON body. Add(field, True)and(field, False)for both fields; all four must fail before the fix._milliseconds_to_seconds, rejectisinstance(value, bool)before theif not valueguard, raisingInvalidClientTimingwith a message naming the field, consistent with the existing wording.grep -rn "client_timeout\|client_sent_msg_ts" src/interfaces/chat_app/static/ src/interfaces/chat_app/openai_compat.py— the JS client sendsDate.now()and a number, andopenai_compat.pysynthesizesnow.timestamp().api_reference.mdrows.Commands
Acceptance criteria
python -c "from src.interfaces.chat_app.request_validation import parse_client_timeout as p; p(True)"exits non-zero withInvalidClientTiming; same forp(False),parse_client_sent_msg_ts(True),parse_client_sent_msg_ts(False).parse_client_timeout(600000) == 600.0andparse_client_sent_msg_ts(1700000000000) == 1700000000.0.0—parse_client_timeout(None) == 0. Rejectingfalsemust not break omission.grep -c boolean docs/docs/api_reference.md≥ 1.git diff origin/dev -- docs/docs/api_reference.md | grep -c '#L[0-9]'returns0.bash scripts/gate.shpasses with ≥80% diff coverage;openspec validate <change> --strictpasses.Start here
Run the reproduce command above and confirm it prints
0.001 0. Then read_milliseconds_to_secondsand note that the falsey guard precedes the division — that ordering is why the check has to go first.Found by adversarial review of the #194 docs PR. Related: #190 (anchor drift), #193 (nullable timing column), #194 (the docs rows this would let us strengthen).