Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/agents/github-operator.agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,10 @@ Do not jump directly to raw API calls if a dedicated MCP or CLI command covers t
2. Read any local specification or context file needed to perform the task correctly.
3. Load the relevant repository skill when one exists.
4. Choose the highest-level GitHub interface that can perform the task safely.
5. Execute the operation with the minimum number of calls needed.
6. Verify the result by reading the updated GitHub object or returned URL.
7. Report only the outcome and key identifiers back to the caller.
5. For PR descriptions, reconcile the proposed body with the actual branch diff and commit list before applying updates.
6. Execute the operation with the minimum number of calls needed.
7. Verify the result by reading the updated GitHub object or returned URL.
8. Report only the outcome and key identifiers back to the caller.

## Repository Guidance

Expand All @@ -58,6 +59,7 @@ Do not jump directly to raw API calls if a dedicated MCP or CLI command covers t
- Do not assume the visible issue number is the same identifier required by a GitHub API.
- For sub-issue linking, remember that the REST API expects the child issue's internal GitHub ID,
not its visible issue number.
- Do not claim PR implementation changes that are not present in the current HEAD diff.
- Do not mix GitHub task execution with unrelated code changes.
- If a PR review comment requires code changes, stop after identifying the actionable request and
hand control back to the caller or a code-focused agent.
Expand All @@ -70,3 +72,4 @@ When finishing a task, return:
1. What was changed or verified
2. The key GitHub identifiers or URLs
3. Any blockers, permissions issues, or follow-up needed
4. For PR body updates, a short evidence line showing the checked commit range and changed files
16 changes: 16 additions & 0 deletions .github/skills/dev/git-workflow/open-pull-request/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Before opening a PR:
- [ ] Branch is pushed to your fork remote
- [ ] Commits are GPG signed (`git log --show-signature -n 1`)
- [ ] All pre-commit checks passed (`linter all`, `cargo machete`, tests)
- [ ] PR body claims are aligned with the actual commit range (`origin/develop..HEAD`)
- [ ] If manual verification used temporary local-only patches, PR body explicitly says they are not included

> Important: always open the PR in the **upstream repository**, not in your fork.
> Resolve upstream from `Cargo.toml` (`repository = "https://github.com/torrust/torrust-tracker"`) and use that value for `gh pr create --repo ...`.
Expand All @@ -42,6 +44,11 @@ PR body must include:
- Validation performed
- Issue link (`Closes #<issue-number>`)

PR body must not include:

- Claims about code changes that are not present in the branch diff
- Ambiguous wording that mixes temporary local verification patches with committed implementation

## Option A (Preferred): GitHub CLI

```bash
Expand Down Expand Up @@ -76,6 +83,15 @@ When MCP pull request management tools are available, create the PR with:
- [ ] Head branch is correct
- [ ] CI workflows started
- [ ] Issue linked in description
- [ ] PR body still matches branch diff and commit history after final rebases/edits

Quick body-accuracy verification:

```bash
gh pr view <pr-number> --repo <upstream-owner>/<upstream-repo> --json body
git diff --name-only origin/develop...HEAD
git log --oneline origin/develop..HEAD
```

## Troubleshooting

Expand Down
6 changes: 6 additions & 0 deletions console/tracker-client/src/console/clients/udp/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@
//! }
//! ```
//!
//! Unrecognized UDP response:
//!
//! ```text
//! Error: Unrecognized UDP tracker response. Expected a valid UDP response, got: [0, 0, 0, 1]
//! ```
//!
//! You can use an URL with instead of the socket address. For example:
//!
//! ```text
Expand Down
58 changes: 50 additions & 8 deletions console/tracker-client/src/console/clients/udp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,35 @@ pub enum Error {
#[error("Failed to send a connection request, with error: {err}")]
UnableToSendConnectionRequest { err: udp::Error },

#[error("Failed to receive a connect response, with error: {err}")]
UnableToReceiveConnectResponse { err: udp::Error },
#[error("{err}")]
UnableToReceiveConnectResponse {
#[source]
err: udp::Error,
},

#[error("Failed to send a announce request, with error: {err}")]
UnableToSendAnnounceRequest { err: udp::Error },

#[error("Failed to receive a announce response, with error: {err}")]
UnableToReceiveAnnounceResponse { err: udp::Error },
#[error("{err}")]
UnableToReceiveAnnounceResponse {
#[source]
err: udp::Error,
},

#[error("Failed to send a scrape request, with error: {err}")]
UnableToSendScrapeRequest { err: udp::Error },

#[error("Failed to receive a scrape response, with error: {err}")]
UnableToReceiveScrapeResponse { err: udp::Error },
#[error("{err}")]
UnableToReceiveScrapeResponse {
#[source]
err: udp::Error,
},

#[error("Failed to receive a response, with error: {err}")]
UnableToReceiveResponse { err: udp::Error },
#[error("{err}")]
UnableToReceiveResponse {
#[source]
err: udp::Error,
},

#[error("Failed to get local address for connection: {err}")]
UnableToGetLocalAddr { err: udp::Error },
Expand All @@ -48,3 +60,33 @@ impl From<Error> for String {
value.to_string()
}
}

#[cfg(test)]
mod tests {
use std::io;
use std::sync::Arc;

use bittorrent_tracker_client::udp;

use super::Error;

#[test]
fn it_should_display_the_inner_udp_parse_error_for_announce_responses() {
// Arrange
let inner_error = udp::Error::UnableToParseResponse {
err: Arc::new(io::Error::new(io::ErrorKind::Other, "failed to fill whole buffer")),
response: vec![0, 0, 0, 1],
};

let error = Error::UnableToReceiveAnnounceResponse { err: inner_error };

// Act
let message = error.to_string();

// Assert
assert_eq!(
message,
"Unrecognized UDP tracker response. Expected a valid UDP response, got: [0, 0, 0, 1]"
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,16 +108,104 @@ UnableToReceiveAnnounceResponse { err: udp::Error },
In `console/tracker-client/src/console/clients/udp/app.rs`, add an example showing what
the error output looks like when an unrecognized response is received.

## Manual Verification

This section is a living test plan and result log for validating the implementation against real
UDP trackers.

### Goal

- Confirm that the CLI prints a clean, readable error when a UDP tracker returns bytes that cannot
be parsed into a known response.
- Confirm whether the issue can be reproduced with real-world public trackers from the newtrackon
UDP list.
- If all sampled trackers return valid responses, record that outcome here and switch to the
fallback plan described later in the issue discussion.

### Step 1: Collect stable UDP trackers

- Query the newtrackon UDP endpoint: <https://newtrackon.com/api#get-/udp>
- Record the returned tracker list used for the verification run.
- Note the date, time, and any filtering applied before testing.

### Step 2: Probe each tracker with a sample request

- Send a representative UDP request to each tracker in the sampled list.
- Record whether the tracker returns a valid UDP response or an unrecognized payload.
- For invalid responses, record the raw bytes exactly as printed by the CLI.

### Step 3: Record results

Use this table to track progress and outcomes:

| Tracker | Sample request | Result | Notes |
| ------------------------------------------ | --------------------------------------------------- | ------ | --------------------------------- |
| `udp://tracker.dler.com:6969/announce` | `announce 9c38422213e30bff212b30c360d26f9a02136422` | valid | Returned announce JSON with peers |
| `udp://tracker.tryhackx.org:6969/announce` | `announce 9c38422213e30bff212b30c360d26f9a02136422` | valid | Returned announce JSON with peers |
| `udp://tracker.fnix.net:6969/announce` | `announce 9c38422213e30bff212b30c360d26f9a02136422` | valid | Returned announce JSON |
| `udp://evan.im:6969/announce` | `announce 9c38422213e30bff212b30c360d26f9a02136422` | valid | Returned announce JSON |

Observed on 2026-05-11.

### Step 4: Decide next action

- The sampled newtrackon trackers returned valid UDP responses.
- No malformed payload has been observed yet, so the real-tracker path is currently not enough to
exercise the unrecognized-response display branch.

### Step 5: Local invalid-response verification

If the public trackers stay valid, use a local tracker instance to force a malformed UDP response
and verify the CLI output end-to-end.

1. Change the code of the UDP tracker in the local code so it returns a deliberately malformed
UDP payload.
2. Run the UDP tracker locally.
3. Make the request to the locally running tracker with the UDP tracker client.
4. Verify the client cannot parse the response and prints useful information, including the
malformed bytes, so the user can understand what happened.

Observed local verification on 2026-05-11:

Tracker start command (with a temporary local patch applied in the UDP server
send path to force payload `[0, 0, 0, 1]`):

```bash
cargo run
```

Client probe command:

```bash
target/debug/udp_tracker_client announce \
udp://127.0.0.1:6969/announce \
9c38422213e30bff212b30c360d26f9a02136422
```

Observed client output:

```text
Error: Unrecognized UDP tracker response. Expected a valid UDP response,
got: [0, 0, 0, 1]

Caused by:
0: Unrecognized UDP tracker response. Expected a valid UDP response,
got: [0, 0, 0, 1]
1: invalid data
```

Result: malformed bytes are visible in CLI output as required.

## Acceptance Criteria

- [ ] Running the client against a tracker that returns an invalid packet produces output
- [x] Running the client against a tracker that returns an invalid packet produces output
matching:
`Error: Unrecognized UDP tracker response. Expected a valid UDP response, got: [...]`
- [ ] Running the client against a well-behaved tracker still prints the JSON response and
- [x] Running the client against a well-behaved tracker still prints the JSON response and
exits `0`
- [ ] `linter all` exits with code `0`
- [ ] `cargo machete` reports no unused dependencies
- [ ] All existing tests pass
- [x] `linter all` exits with code `0`
- [x] `cargo machete` reports no unused dependencies
- [x] All existing tests pass

## Key Files

Expand Down
34 changes: 32 additions & 2 deletions packages/tracker-client/src/udp/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,42 @@ pub enum Error {
#[error("Failed to get data from request: {request:?}, with error: {err:?}")]
UnableToWriteDataFromRequest { err: Arc<std::io::Error>, request: Request },

#[error("Failed to parse response: {response:?}, with error: {err:?}")]
UnableToParseResponse { err: Arc<std::io::Error>, response: Vec<u8> },
#[error("Unrecognized UDP tracker response. Expected a valid UDP response, got: {response:?}")]
UnableToParseResponse {
#[source]
err: Arc<std::io::Error>,
response: Vec<u8>,
},
}

impl From<Error> for DynError {
fn from(e: Error) -> Self {
Arc::new(Box::new(e))
}
}

#[cfg(test)]
mod tests {
use std::io;
use std::sync::Arc;

use super::Error;

#[test]
fn it_should_display_unrecognized_udp_tracker_response_without_debug_noise() {
// Arrange
let error = Error::UnableToParseResponse {
err: Arc::new(io::Error::new(io::ErrorKind::Other, "failed to fill whole buffer")),
response: vec![0, 0, 0, 1],
};

// Act
let message = error.to_string();

// Assert
assert_eq!(
message,
"Unrecognized UDP tracker response. Expected a valid UDP response, got: [0, 0, 0, 1]"
);
}
}
3 changes: 3 additions & 0 deletions project-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ datetime
dbip
dbname
debuginfo
dler
Deque
Dihc
Dijke
Expand All @@ -88,6 +89,7 @@ fdbased
fdget
filesd
finalises
fnix
flamegraph
formatjson
fput
Expand Down Expand Up @@ -218,6 +220,7 @@ recognised
recompiles
referer
Registar
tryhackx
repomix
repr
reqs
Expand Down
Loading