From f97b77fd729719a87a63ff29b03d5187d9061217 Mon Sep 17 00:00:00 2001 From: JB Date: Sun, 8 Mar 2026 21:32:26 -0700 Subject: [PATCH] Add --verbose opt --- README.md | 4 + hydra-cli/src/hydra/reqwest_client.rs | 108 ++++++++++++++++++++------ hydra-cli/src/main.rs | 8 +- 3 files changed, 96 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 3037682..8087d9a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ USAGE: FLAGS: -h, --help Prints help information --no-check-certificate Disable TLS certificate check for the Hydra host + --verbose Print verbose diagnostics including HTTP responses -V, --version Prints version information OPTIONS: @@ -81,6 +82,9 @@ A client to query Hydra through its JSON API. The `project-create` command creates a new project under the name specified. The created project will be _enabled_ and _visible_. _Note_: this command requires user authentication. +Use `--verbose` to print diagnostic output, including full HTTP status, headers, and body for each +server response during the command. + `$ hydra-cli project-create --help` ``` hydra-cli-project-create diff --git a/hydra-cli/src/hydra/reqwest_client.rs b/hydra-cli/src/hydra/reqwest_client.rs index f0b2104..119a255 100644 --- a/hydra-cli/src/hydra/reqwest_client.rs +++ b/hydra-cli/src/hydra/reqwest_client.rs @@ -1,6 +1,7 @@ use crate::hydra::client::*; use reqwest::blocking::Client as ReqwestClient; +use reqwest::blocking::Response; use reqwest::header::{CONTENT_TYPE, REFERER}; use serde::de::DeserializeOwned; use serde_json::Value; @@ -24,30 +25,86 @@ impl From for ClientError { pub struct Client { pub host: String, pub client: ReqwestClient, + pub verbose: bool, } impl Client { pub fn new(client: ReqwestClient, host: String) -> Client { - Client { client, host } + Client::new_with_verbose(client, host, false) + } + + pub fn new_with_verbose(client: ReqwestClient, host: String, verbose: bool) -> Client { + Client { + client, + host, + verbose, + } + } +} + +fn log_response( + status: reqwest::StatusCode, + headers: &[(String, String)], + body: &str, +) { + println!("< HTTP {}", status); + for (header_name, header_value) in headers.iter() { + println!( + "< {}: {}", + header_name, + header_value + ); + } + println!("<"); + print!("{}", body); + if !body.ends_with('\n') { + println!(); } } /// Performs a GET request retrieving a deserializable response -fn get_json(client: &ReqwestClient, url: &str) -> Result { +fn get_json( + client: &ReqwestClient, + url: &str, + verbose: bool, +) -> Result { let res = client .get(url) .header(reqwest::header::CONTENT_TYPE, "application/json") .send()?; + let (status, body) = read_response_body(res, verbose)?; - if res.status().is_success() { - let v: Value = res.json()?; + if status.is_success() { + let v: Value = serde_json::from_str(&body) + .map_err(|e| ClientError::Error(format!("error decoding response body: {}", e)))?; match serde_json::from_value(v) { Ok(x) => Ok(x), Err(x) => Err(ClientError::InvalidResponse(format!("{}", x))), } } else { - Err(ClientError::Error(format!("{}", res.status()))) + Err(ClientError::Error(format!("{}", status))) + } +} + +fn read_response_body(res: Response, verbose: bool) -> Result<(reqwest::StatusCode, String), ClientError> { + let status = res.status(); + let headers = res + .headers() + .iter() + .map(|(name, value)| { + ( + name.as_str().to_string(), + value.to_str().unwrap_or("").to_string(), + ) + }) + .collect::>(); + let body = res.text()?; + + if verbose { + log_response(status, &headers, &body); } + + Ok((status, body)) } impl HydraClient for Client { @@ -64,11 +121,12 @@ impl HydraClient for Client { .header(REFERER, self.host.as_str()) .json(&proj) .send()?; + let (status, _) = read_response_body(res, self.verbose)?; - if res.status().is_success() { + if status.is_success() { Ok(()) } else { - Err(ClientError::Error(format!("{}", res.status()))) + Err(ClientError::Error(format!("{}", status))) } } @@ -76,27 +134,27 @@ impl HydraClient for Client { self.host.clone() } fn projects(&self) -> Result, ClientError> { - get_json(&self.client, &self.host) + get_json(&self.client, &self.host, self.verbose) } fn search(&self, query: &str) -> Result { let request_url = format!("{}/search?query={}", &self.host, query); - get_json(&self.client, &request_url) + get_json(&self.client, &request_url, self.verbose) } fn jobset_overview(&self, project: &str) -> Result, ClientError> { let request_url = format!("{}/api/jobsets?project={}", &self.host, project); - get_json(&self.client, &request_url) + get_json(&self.client, &request_url, self.verbose) } fn jobset(&self, project: &str, jobset: &str) -> Result { let request_url = format!("{}/jobset/{}/{}", &self.host, project, jobset); - get_json(&self.client, &request_url) + get_json(&self.client, &request_url, self.verbose) } fn eval(&self, number: i64) -> Result { let request_url = format!("{}/eval/{}", &self.host, number); - get_json(&self.client, &request_url) + get_json(&self.client, &request_url, self.verbose) } fn jobset_create( @@ -112,11 +170,12 @@ impl HydraClient for Client { .header(REFERER, self.host.as_str()) .json(&jobset_config) .send()?; + let (status, _) = read_response_body(res, self.verbose)?; - if res.status().is_success() { + if status.is_success() { Ok(()) } else { - Err(ClientError::Error(format!("{}", res.status()))) + Err(ClientError::Error(format!("{}", status))) } } @@ -128,11 +187,12 @@ impl HydraClient for Client { .header(REFERER, self.host.as_str()) .header(CONTENT_TYPE, "application/json") .send()?; + let (status, _) = read_response_body(res, self.verbose)?; - if res.status().is_success() { + if status.is_success() { Ok(()) } else { - Err(ClientError::Error(format!("{}", res.status()))) + Err(ClientError::Error(format!("{}", status))) } } @@ -146,11 +206,12 @@ impl HydraClient for Client { .put(&request_url) .header(REFERER, self.host.as_str()) .send()?; + let (status, _) = read_response_body(res, self.verbose)?; - if res.status().is_success() { + if status.is_success() { Ok(()) } else { - Err(ClientError::Error(format!("{}", res.status()))) + Err(ClientError::Error(format!("{}", status))) } } @@ -165,12 +226,13 @@ impl HydraClient for Client { match login_res { Ok(r) => { - if r.status().is_success() { + let (status, _) = read_response_body(r, self.verbose)?; + if status.is_success() { Ok(()) - } else if r.status().is_redirection() { + } else if status.is_redirection() { Ok(()) } else { - Err(ClientError::Error(format!("Response Error: {}", r.status()))) + Err(ClientError::Error(format!("Response Error: {}", status))) } } Err(err) => Err(ClientError::Error(format!("Request Error: {}", err))), @@ -202,7 +264,7 @@ mod tests { .create(); let c = client(); - let res: Result = get_json(&c.client, &c.host); + let res: Result = get_json(&c.client, &c.host, false); assert_eq!( res, Err(ClientError::Error("500 Internal Server Error".to_string())) @@ -218,7 +280,7 @@ mod tests { .create(); let c = client(); - let res: Result, ClientError> = get_json(&c.client, &c.host); + let res: Result, ClientError> = get_json(&c.client, &c.host, false); assert_eq!( res, diff --git a/hydra-cli/src/main.rs b/hydra-cli/src/main.rs index 6042c14..c1e504d 100644 --- a/hydra-cli/src/main.rs +++ b/hydra-cli/src/main.rs @@ -35,6 +35,11 @@ fn main() { .long("no-check-certificate") .help("Disable TLS certificate check for the Hydra host"), ) + .arg( + Arg::with_name("verbose") + .long("verbose") + .help("Print verbose diagnostics including HTTP responses"), + ) .subcommand( SubCommand::with_name("search") .about("Search by output paths") @@ -208,6 +213,7 @@ fn main() { let matches = app.get_matches(); let host = matches.value_of("host").unwrap(); let no_check_certs = matches.is_present("no-check-certificate"); + let verbose = matches.is_present("verbose"); let custom = redirect::Policy::none(); @@ -217,7 +223,7 @@ fn main() { .redirect(custom) .build() .unwrap(); - let client = ReqwestHydraClient::new(c, String::from(host)); + let client = ReqwestHydraClient::new_with_verbose(c, String::from(host), verbose); let cmd_res: OpResult = match matches.subcommand() { ("search", Some(args)) => search::run(