host: claim GET ut/%s/item (FutViewCards), which fell through to Python

Fifth instance of this project's recurring dead-route defect: a handler exists and
is correct, but classify() never produces the route, so every request falls
through to the Python upstream. Invisible in production, where the oracle answers;
on staging, where the upstream is deliberately dead, it is a 502.

GET ut/%s/item is FutViewCards (deser 0x1801293d0, top-level itemData via the
shared card element 0x18013fe00). The oracle answers it with defs_route - the SAME
handler already wired for item/resource and defid: pull every integer out of the
query (idList=a,b,c / definitionId= / resourceId=) and return one definition per
id, or {"itemData": []} when there are none (tools/utas_server.py:1103). So
claiming it is byte-identical parity, not new behaviour.

The query handling is the substance of the fix, not decoration. ut_tail does NOT
strip the query string, so an equality-only arm (`Some("item")`) misses every real
request while passing a no-query unit test - which is precisely how the route
stayed unclaimed. The same latent bug applied to the two arms that were already
there: item/resource and defid only matched with no query, so
`item/resource?resourceId=` and `defid?definitionId=` were ALSO falling through.
All three now mirror the oracle's own `item(\?|$)` pattern.

Verified on staging, all 200 where they were 502:
  GET /item                                  -> itemData 0
  GET /item?idList=100000003,100000004       -> itemData 2
  GET /ut/v2/game/fifa17/item                -> itemData 0
  GET /item/resource?resourceId=5003012      -> itemData 1
  GET /defid?definitionId=200389             -> itemData 1

Does NOT by itself fix the kit selector - GET /item is a definition lookup keyed
by ids the client already holds, not the thing that seeds the club collection, and
the observed session never requested it. It is a real production-masked gap and a
prerequisite for testing anything else on staging.

get_item_is_claimed_and_the_other_item_verbs_are_unaffected pins the claim plus
the three verbs that share the prefix: PUT item stays FutMoveCard on the economy
path and DELETE item/<id> stays QuickSellPath.

cargo test -p openfut-utas-host: 190 passed, 0 failed. clippy -D warnings clean.
This commit is contained in:
funman300
2026-08-23 20:22:54 +00:00
parent 6baa673252
commit 9cc5188e5c
+72 -2
View File
@@ -292,8 +292,32 @@ pub fn classify(method: &str, path: &str) -> Route {
Some("champion") if get => Route::FeatureOffEmpty,
Some("clubUser") if get => Route::FeatureOffEmpty,
Some("user/list") if get => Route::FeatureOffEmpty,
Some("item/resource") if get => Route::ItemDefs,
Some("defid") if get => Route::ItemDefs,
Some(t) if get && (t == "item/resource" || t.starts_with("item/resource?")) => {
Route::ItemDefs
}
Some(t) if get && (t == "defid" || t.starts_with("defid?")) => Route::ItemDefs,
// `GET ut/%s/item` is FutViewCards (deser `0x1801293d0`, top-level
// `itemData` via the shared card element `0x18013fe00`). The oracle
// answers it with `defs_route` — the SAME handler as `item/resource` and
// `defid`: parse every integer out of the query (`idList=a,b,c`,
// `definitionId=`, `resourceId=`) and return one definition per id, or
// `{"itemData": []}` when the query carries none
// (`tools/utas_server.py:1103`). Claiming it is byte-identical parity.
//
// It was unclaimed, so it fell through to the Python upstream. That is
// invisible in production, where the oracle answers, and shows up only on
// staging as a 502 — the same dead-route class already found four times
// (season/list, watchList, the handle_static_ack tails, tournament/user).
//
// The `?` forms are not decoration: `ut_tail` does NOT strip the query, so
// a bare equality arm silently misses every real request, which is exactly
// how this route stayed unclaimed. The oracle's own pattern is
// `item(\?|$)` (`tools/utas_server.py:1419`) and this mirrors it.
//
// MUST stay below `item/resource` (matched first) and must not swallow
// `item/<id>`, which is DELETE-only Quick Sell, nor PUT `item`, which is
// FutMoveCard on the economy path.
Some(t) if get && (t == "item" || t.starts_with("item?")) => Route::ItemDefs,
Some(t) if get && (t == "marketdata" || t.starts_with("marketdata/")) => Route::MarketData,
_ => Route::Passthrough,
}
@@ -7045,6 +7069,52 @@ mod tests {
Route::Passthrough
);
}
/// `GET ut/%s/item` (FutViewCards) MUST be claimed, and claiming it must not
/// disturb the three other verbs that share the `item` prefix.
///
/// This route was unclaimed and fell through to the Python upstream, which is
/// invisible in production (the oracle answers) and appears only on staging as
/// a 502. That is the recurring dead-route defect in this project, so the
/// point of this test is that a handler existing is not the same as a request
/// reaching it.
#[test]
fn get_item_is_claimed_and_the_other_item_verbs_are_unaffected() {
// FutViewCards, with and without the definition query the client builds.
assert_eq!(classify("GET", "/ut/game/fifa17/item"), Route::ItemDefs);
assert_eq!(
classify("GET", "/ut/game/fifa17/item?idList=100000003,100000004"),
Route::ItemDefs
);
// v2 sku form resolves identically.
assert_eq!(classify("GET", "/ut/v2/game/fifa17/item"), Route::ItemDefs);
// The more specific definition routes still win, and — the bug this test
// exists for — they must survive a query string too. `ut_tail` does not
// strip the query, so an equality-only arm misses every real request while
// looking correct in a no-query unit test.
assert_eq!(
classify("GET", "/ut/game/fifa17/item/resource"),
Route::ItemDefs
);
assert_eq!(
classify("GET", "/ut/game/fifa17/item/resource?resourceId=5003012"),
Route::ItemDefs
);
assert_eq!(
classify("GET", "/ut/game/fifa17/defid?definitionId=200389"),
Route::ItemDefs
);
// PUT `item` is FutMoveCard on the economy path, NOT a definition read.
assert_eq!(classify("PUT", "/ut/game/fifa17/item"), Route::Passthrough);
assert_eq!(
classify_economy("PUT", "/ut/game/fifa17/item"),
Some(EconomyRoute::MoveItems)
);
// DELETE `item/<id>` is single-card Quick Sell and must not be swallowed.
assert_eq!(
classify_economy("DELETE", "/ut/game/fifa17/item/100000240"),
Some(EconomyRoute::QuickSellPath)
);
}
/// Retail FIFA 17 issues part of the item family under `/ut/v2/game/`, so the
/// same three verbs must land identically under both prefixes: `ut_tail`