Skip to content

Latest commit

 

History

History
246 lines (190 loc) · 11.1 KB

File metadata and controls

246 lines (190 loc) · 11.1 KB
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
skill-links related-artifacts
create-issue
docs/issues/README.md
console/tracker-client/
packages/http-tracker-core/

Issue #672 — HTTP Tracker Client: Print Unrecognized Responses in JSON

Overview

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.

Motivation

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.

Current Behaviour

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.

Proposed Behaviour

The two-step fallback strategy:

  1. Try to deserialize into the typed struct (existing behaviour).
  2. On failure, convert the raw bencoded bytes to generic JSON using the bencode2json crate and print that instead.
  3. 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, ...]

Goals

  • Replace both panic!(...) / .unwrap_or_else(|_| panic!(...)) calls in app.rs with graceful fallback logic
  • Remove panic/unwrap usage from the scrape decode path: expect(...) in try_from_bencoded and nested .unwrap() calls in parser helpers
  • Add bencode2json as a dependency of the torrust-tracker-client console crate
  • On deserialization failure, print the raw bencoded payload as generic JSON (via bencode2json)
  • If bencode2json conversion 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 Err from the command function)
  • Fallback JSON output is compact by default in this issue; once --format is introduced in #1562, fallback JSON must respect the selected format
  • linter all exits with code 0
  • cargo machete reports no unused dependencies
  • All existing tests pass

Implementation Plan

Task 1: Fix scrape::Response::try_from_bencoded to not panic

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.

Task 2: Add bencode2json dependency

In console/tracker-client/Cargo.toml, add:

bencode2json = "0.1"   # adjust to the published version

Task 3: Implement the two-step fallback helper

Add 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:?}"),
    }
}

Task 4: Replace panics in announce_command

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"))
    }
}

Task 5: Replace panics in scrape_command

Apply the same two-step fallback to scrape_command, replacing the current .unwrap_or_else(|_| panic!(...)).

Task 6: Update the module doc comment in app.rs

Add examples showing the fallback output in the module-level doc comment.

Manual Verification

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.

Verification Date

  • 2026-05-11

Commands And Results

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

Notes

  • 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.

Acceptance Criteria

  • 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 all exits with code 0
  • cargo machete reports no unused dependencies
  • All existing tests pass

Key Files

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

References