Skip to content

feat(0119): reject every invalid input with a 400 envelope - #217

Open
stkrolikiewicz wants to merge 5 commits into
developfrom
feat/0119_api-input-validation-hardening
Open

feat(0119): reject every invalid input with a 400 envelope#217
stkrolikiewicz wants to merge 5 commits into
developfrom
feat/0119_api-input-validation-hardening

Conversation

@stkrolikiewicz

Copy link
Copy Markdown
Collaborator

Summary

  • Custom ValidatedQuery/ValidatedJson/ValidatedPath extractors route every axum rejection through the standard ErrorEnvelope — the old text/plain 400/415/422 bodies are gone; 400s now carry Cache-Control: no-store, and POST /prices/batch gets an explicit 16 KB body limit
  • Query params are typed serde enums (the six stringly parse_* fns deleted): unknown tokens 400 with the valid values enumerated, search is capped at 1–12 ASCII alphanumerics, and the same enums publish into OpenAPI (contract-tested — the test caught dangling $refs utoipa-axum does not auto-register)
  • Pagination cursor: deny_unknown_fields, 256-char token cap, and payload type-checked against the active sort — closes a client-reachable 500 (corrupt v reaching ClickHouse toFloat64)
  • OHLCV: real date parsing (chrono, already in the build graph) with validated epochs bound via toDateTime(?), plus an explicit window rule (start < end, ≤5000 points at the chosen granularity) replacing silent truncation; the timeframe window anchors to end when only end is given
  • 43 new CH-less negative tests drive the real router with AppState::without_ch() (panics on any CH access, proving validation precedes every query) and run in the existing CI cargo test step; prices-api added to the clippy -D warnings gate; all 21 live-CH integration tests pass against the prod-pinned 26.3.10.60

Task: lore/1-tasks/active/0119_FEATURE_api-input-validation-hardening.md (param → rule → error-code table + recorded policies)

Axum's own Query/Json/Path rejections answered in text/plain with 415/422
for body failures, bypassing the ErrorEnvelope contract. New wrapper
extractors (ValidatedQuery/ValidatedJson/ValidatedPath) map every
rejection to 400 + envelope; new invalid_body code; bad_request now
carries Cache-Control: no-store; /prices/batch gets an explicit 16 KB
body limit so an oversized body is refused before parsing. CH-less
negative tests prove each rejection fires before any ClickHouse call.
… fns

The six param enums (SortCol, Order, TypeFilter, Timeframe, Granularity,
BaseCurrency) now derive Deserialize + ToSchema with explicit per-variant
renames, so params deserialize straight into typed forms: an unknown token
fails serde with a message enumerating the valid values (400 envelope via
ValidatedQuery), and the same enums publish into OpenAPI. Deletes the six
stringly parse() fns. Case policy: exact documented tokens; base_currency
keeps lowercase usd/xlm aliases (historically case-insensitive). New:
search capped at 1-12 ASCII alphanumeric (SEP-11 alphanum12 prefix).
Cursor: deny_unknown_fields (foreign lookalikes 400), 256-char token cap,
and valid_for(sort) — a numeric sort requires a finite-parseable v, which
previously reached ClickHouse toFloat64() and threw a 500 from client
input; code sorts require an asset-code shape.

OHLCV: parse_time (chrono; already in the build graph transitively)
replaces the shape-only valid_iso8601, rejecting impossible dates that CH
would have interpreted freely; the validated epoch (not the raw string)
is bound via toDateTime(?) so exactly one interpretation of the window
exists. New window rule before any DB call: start < end, and
ceil(span/granularity) <= OHLCV_MAX_POINTS with a message naming the
count — replacing the silent newest-5000 truncation. The timeframe
window anchors to end when only end is given (?end=..&timeframe=7d is
the 7d window ending there); timeframe=all starts at Stellar genesis.
…s-api

Registers the six query-param enums in ApiDoc components — utoipa-axum
collects body schemas from routes but a params-tuple $ref is NOT
auto-registered, so the served document carried dangling refs (caught by
the new enum-publication contract test). BatchRequest.assets publishes
minItems/maxItems mirroring MAX_BATCH; search publishes its 1-12 length
bounds. Task file gains the AC 1 param -> rule -> error-code table and
the recorded policies (unknown params ignored, exact-token case with the
base_currency lowercase exception, cursor sort-binding limitation,
CH-in-CI deliberately deferred to 0120/0122). CI clippy line now
includes prices-api.
…eframe cliff

Local-CH smoke caught the one positive-path change 0119 makes:
timeframe=all&granularity=1h is now an up-front 400 (genesis to now is
~95k hourly buckets against the 5000 cap), where the merge test leaned
on the old silent newest-5000 truncation. Narrowed with explicit
start/end around the seeded candles — merge + backfill_note coverage
unchanged. All 21 integration tests pass against the prod-pinned CH
26.3.10.60, confirming the toDateTime epoch binds. Task notes record
the consumer-visible change and the ~2029-06 cliff where bare
timeframe=all at 1d itself crosses 5000 buckets.
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.

1 participant