| doc-type | issue | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| issue-type | feature | ||||||||
| status | done | ||||||||
| priority | p3 | ||||||||
| github-issue | 672 | ||||||||
| spec-path | docs/issues/closed/672-http-tracker-client-print-unrecognized-responses.md | ||||||||
| branch | |||||||||
| related-pr | |||||||||
| last-updated-utc | |||||||||
| semantic-links |
|
When the HTTP tracker client's announce or scrape command receives a response body that
cannot be deserialized into the expected Rust struct, the application currently panics with
an unhelpful message. The goal of this issue is to handle that failure gracefully: instead of
panicking, the client should attempt to convert the raw bencoded payload to a generic JSON
representation and print it. If even that conversion fails, the raw bytes should be printed.
- GitHub issue: #672
- Parent EPIC: #669
- Depends on: #673 (bencode-to-JSON
conversion — already resolved:
bencode2jsoncrate published at https://crates.io/crates/bencode2json) - Related: #671 (same feature for UDP client)
Real-world HTTP trackers often return valid but non-standard bencoded responses. For example,
the scrape response from open.acgnxtracker.com omits the downloaded field, which is
required by the Torrust scrape::File struct. This causes:
thread 'main' panicked at packages/tracker-client/src/http/client/responses/scrape.rs:143:60:
called `Result::unwrap()` on an `Err` value: MissingFileField { field_name: "downloaded" }
When testing the client against multiple trackers (e.g. from https://newtrackon.com/), any non-standard response crashes the process without showing what the tracker actually sent.
Both announce_command and scrape_command in
console/tracker-client/src/console/clients/http/app.rs use .unwrap_or_else(|_| panic!(...)):
// announce_command:
let announce_response: Announce = serde_bencode::from_bytes(&body)
.unwrap_or_else(|_| panic!("response body should be a valid announce response, got: \"{body:#?}\""));
// scrape_command:
let scrape_response = scrape::Response::try_from_bencoded(&body)
.unwrap_or_else(|_| panic!("response body should be a valid scrape response, got: \"{body:#?}\""));scrape::Response::try_from_bencoded also panics internally via
serde_bencode::from_bytes(bytes).expect(...).
The scrape parser path also contains nested .unwrap() calls while iterating
decoded file dictionaries. Those must be removed from reachable runtime paths.
The two-step fallback strategy:
- Try to deserialize into the typed struct (existing behaviour).
- On failure, convert the raw bencoded bytes to generic JSON using the
bencode2jsoncrate and print that instead. - If bencode-to-JSON conversion also fails, print the raw bytes in their debug form so the developer can see what was received.
Example output when the response is non-standard but valid bencode:
{
"files": {
"<info_hash_bytes>": {
"incomplete": 0,
"complete": 32
}
}
}Example output when even bencode parsing fails (raw bytes):
Warning: Could not deserialize HTTP tracker response. Raw bytes: [100, 56, ...]
- Replace both
panic!(...)/.unwrap_or_else(|_| panic!(...))calls inapp.rswith graceful fallback logic - Remove panic/unwrap usage from the scrape decode path:
expect(...)intry_from_bencodedand nested.unwrap()calls in parser helpers - Add
bencode2jsonas a dependency of thetorrust-tracker-clientconsole crate - On deserialization failure, print the raw bencoded payload as generic JSON (via
bencode2json) - If
bencode2jsonconversion also fails, print a warning with the raw byte slice - The process exits with a non-zero exit code when the response cannot be deserialized
(print the fallback JSON/bytes to stdout, return an
Errfrom the command function) - Fallback JSON output is compact by default in this issue; once
--formatis introduced in #1562, fallback JSON must respect the selected format -
linter allexits with code0 -
cargo machetereports no unused dependencies - All existing tests pass
In packages/tracker-client/src/http/client/responses/scrape.rs, replace the internal
expect(...) with a proper ?-based propagation so callers can handle the error:
pub fn try_from_bencoded(bytes: &[u8]) -> Result<Self, BencodeParseError> {
let scrape_response: DeserializedResponse = serde_bencode::from_bytes(bytes)
.map_err(|e| BencodeParseError::DeserializationError { source: e })?;
Self::try_from(scrape_response)
}A new BencodeParseError variant may be needed for serde_bencode::Error.
Also replace nested .unwrap() calls in scrape parsing helpers with proper
error propagation into BencodeParseError.
In console/tracker-client/Cargo.toml, add:
bencode2json = "0.1" # adjust to the published versionAdd a private helper in console/tracker-client/src/console/clients/http/app.rs:
fn bencode_to_fallback_json(body: &[u8]) -> String {
match bencode2json::to_json(body) {
Ok(json) => json,
Err(_) => format!("(raw bytes) {body:?}"),
}
}let body = response.bytes().await?;
match serde_bencode::from_bytes::<Announce>(&body) {
Ok(announce_response) => {
let json = serde_json::to_string(&announce_response)
.context("failed to serialize announce response into JSON")?;
println!("{json}");
Ok(())
}
Err(_) => {
let fallback = bencode_to_fallback_json(&body);
eprintln!("Warning: Could not deserialize HTTP tracker announce response.");
println!("{fallback}");
Err(anyhow::anyhow!("unrecognized announce response from tracker"))
}
}Apply the same two-step fallback to scrape_command, replacing the current
.unwrap_or_else(|_| panic!(...)).
Add examples showing the fallback output in the module-level doc comment.
Manual verification was performed using temporary local HTTP fixture servers (Python http.server),
without modifying tracker source code. This validates all response-handling branches deterministically.
- 2026-05-11
| Scenario | Command | Output mode | Exit code | Notes |
|---|---|---|---|---|
| Non-standard but valid bencode scrape response | cargo run -p torrust-tracker-client --bin http_tracker_client -- scrape http://127.0.0.1:18080 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
Generic JSON fallback | 1 |
Printed {"foo":"bar"}, then Error: unrecognized scrape response from tracker |
Malformed announce payload (not-bencode-response) |
cargo run -p torrust-tracker-client --bin http_tracker_client -- announce http://127.0.0.1:18080 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
Raw-bytes fallback | 1 |
Printed warning with raw byte slice, then Error: unrecognized announce response from tracker |
| Typed announce payload (tracker-compatible schema) | cargo run -p torrust-tracker-client --bin http_tracker_client -- announce http://127.0.0.1:18082 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
Typed JSON | 0 |
Printed typed JSON including min interval and peers |
| Typed scrape payload (tracker-compatible schema) | cargo run -p torrust-tracker-client --bin http_tracker_client -- scrape http://127.0.0.1:18082 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa |
Typed JSON | 0 |
Printed typed scrape JSON for the provided info-hash |
- Local fixture servers were started in temporary terminals and terminated after validation.
- No temporary response-forcing patch was committed to tracker code.
- This run validates the fallback behavior required by #672 and compatibility with expected typed response schemas.
- Running the client against a tracker that returns a non-standard response prints the
response as generic JSON (via
bencode2json) and exits non-zero - Running the client against a tracker that returns a completely unrecognized payload prints a warning with the raw bytes and exits non-zero
- Running the client against the Torrust Tracker still prints the typed JSON response
and exits
0(not executed in this run; validated with local tracker-compatible typed fixtures) - No
panic!or.unwrap()in the announce or scrape command paths - No reachable panic/unwrap remains in the scrape decoding path
-
linter allexits with code0 -
cargo machetereports no unused dependencies - All existing tests pass
| File | Role |
|---|---|
console/tracker-client/src/console/clients/http/app.rs |
Replace panics with two-step fallback — main change |
packages/tracker-client/src/http/client/responses/scrape.rs |
Fix try_from_bencoded to not panic internally |
console/tracker-client/Cargo.toml |
Add bencode2json dependency |
- Parent EPIC: #669
- Depends on: #673
(bencode-to-JSON, resolved —
bencode2jsonon crates.io) - Related UDP issue: #671
bencode2jsoncrate: https://crates.io/crates/bencode2jsonbencode2jsonsource: https://github.com/torrust/bencode2json- BitTorrent scrape spec: https://www.bittorrent.org/beps/bep_0048.html
- List of public HTTP trackers: https://newtrackon.com/