From 9cc5188e5cd6119c43c5a64a13d4fea410db51b5 Mon Sep 17 00:00:00 2001 From: funman300 Date: Sun, 23 Aug 2026 20:22:54 +0000 Subject: [PATCH] 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/ stays QuickSellPath. cargo test -p openfut-utas-host: 190 passed, 0 failed. clippy -D warnings clean. --- openfut-utas-host/src/lib.rs | 74 +++++++++++++++++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/openfut-utas-host/src/lib.rs b/openfut-utas-host/src/lib.rs index c72278b..8d47a97 100644 --- a/openfut-utas-host/src/lib.rs +++ b/openfut-utas-host/src/lib.rs @@ -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/`, 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/` 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`