diff --git a/.changeset/nasty-roses-trade.md b/.changeset/nasty-roses-trade.md new file mode 100644 index 000000000..d53955e36 --- /dev/null +++ b/.changeset/nasty-roses-trade.md @@ -0,0 +1,5 @@ +--- +"fnm": patch +--- + +better error handling for malformed json diff --git a/Cargo.lock b/Cargo.lock index 6a5f6039a..e21565ea0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -150,6 +150,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + [[package]] name = "base64" version = "0.22.1" @@ -672,6 +681,7 @@ dependencies = [ "indoc", "junction", "log", + "miette 7.2.0", "node-semver", "pretty_assertions", "reqwest", @@ -982,6 +992,12 @@ version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1105,12 +1121,32 @@ version = "5.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" dependencies = [ - "miette-derive", + "miette-derive 5.10.0", "once_cell", "thiserror", "unicode-width", ] +[[package]] +name = "miette" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive 7.2.0", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "thiserror", + "unicode-width", +] + [[package]] name = "miette-derive" version = "5.10.0" @@ -1122,6 +1158,17 @@ dependencies = [ "syn", ] +[[package]] +name = "miette-derive" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "mime" version = "0.3.17" @@ -1162,7 +1209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "84f390c1756333538f2aed01cf280a56bc683e199b9804a504df6e7320d40116" dependencies = [ "bytecount", - "miette", + "miette 5.10.0", "nom", "serde", "thiserror", @@ -1255,6 +1302,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56" + [[package]] name = "pbkdf2" version = "0.12.2" @@ -1797,6 +1850,12 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.5.7" @@ -1825,6 +1884,27 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "supports-color" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8775305acf21c96926c900ad056abeef436701108518cf890020387236ac5a77" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + [[package]] name = "syn" version = "2.0.79" @@ -1884,6 +1964,16 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + [[package]] name = "test-log" version = "0.2.16" @@ -1906,6 +1996,17 @@ dependencies = [ "syn", ] +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.64" @@ -2121,6 +2222,12 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization" version = "0.1.24" diff --git a/Cargo.toml b/Cargo.toml index 3dda990c4..c66a2f3cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ clap_complete = "4.5.2" anyhow = "1.0.86" indicatif = { version = "0.17.8", features = ["improved_unicode"] } flate2 = "1.0.30" +miette = { version = "7.2.0", features = ["fancy"] } [dev-dependencies] pretty_assertions = "1.4.0" diff --git a/e2e/shellcode/shells/cmdEnv.ts b/e2e/shellcode/shells/cmdEnv.ts index 8f2cc5671..c7f0f9d84 100644 --- a/e2e/shellcode/shells/cmdEnv.ts +++ b/e2e/shellcode/shells/cmdEnv.ts @@ -6,6 +6,7 @@ type EnvConfig = { logLevel: string corepackEnabled: boolean resolveEngines: boolean + nodeDistMirror: string } export type HasEnv = { env(cfg: Partial): ScriptLine } @@ -16,6 +17,7 @@ function stringify(envConfig: Partial = {}) { corepackEnabled, resolveEngines, executableName = "fnm", + nodeDistMirror, } = envConfig return [ `${executableName} env`, @@ -23,6 +25,7 @@ function stringify(envConfig: Partial = {}) { logLevel && `--log-level=${logLevel}`, corepackEnabled && "--corepack-enabled", resolveEngines && `--resolve-engines`, + nodeDistMirror && `--node-dist-mirror=${JSON.stringify(nodeDistMirror)}`, ] .filter(Boolean) .join(" ") diff --git a/src/commands/install.rs b/src/commands/install.rs index 86da50e65..61b99a9d2 100644 --- a/src/commands/install.rs +++ b/src/commands/install.rs @@ -210,8 +210,8 @@ pub enum Error { }, #[error("Can't find version in dotfiles. Please provide a version manually to the command.")] CantInferVersion, - #[error("Having a hard time listing the remote versions: {}", source)] - CantListRemoteVersions { source: crate::http::Error }, + #[error(transparent)] + CantListRemoteVersions { source: remote_node_index::Error }, #[error( "Can't find a Node version that matches {} in remote", requested_version diff --git a/src/commands/ls_remote.rs b/src/commands/ls_remote.rs index c7ded6e3b..0b5318969 100644 --- a/src/commands/ls_remote.rs +++ b/src/commands/ls_remote.rs @@ -96,9 +96,9 @@ impl super::command::Command for LsRemote { #[derive(Debug, Error)] pub enum Error { #[error(transparent)] - HttpError { + RemoteListing { #[from] - source: crate::http::Error, + source: remote_node_index::Error, }, } diff --git a/src/http.rs b/src/http.rs index b5e3339a8..57d23a3a1 100644 --- a/src/http.rs +++ b/src/http.rs @@ -2,15 +2,18 @@ //! In the future, if we want to migrate to a different HTTP library, //! we can easily change this facade instead of multiple places in the crate. -use reqwest::blocking::Client; +use reqwest::{blocking::Client, IntoUrl}; -pub type Error = reqwest::Error; +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +#[error(transparent)] +#[diagnostic(code("fnm::http::error"))] +pub struct Error(#[from] reqwest::Error); pub type Response = reqwest::blocking::Response; -pub fn get(url: &str) -> Result { - Client::new() +pub fn get(url: impl IntoUrl) -> Result { + Ok(Client::new() .get(url) // Some sites require a user agent. .header("User-Agent", concat!("fnm ", env!("CARGO_PKG_VERSION"))) - .send() + .send()?) } diff --git a/src/main.rs b/src/main.rs index cfb2a64d7..6ab10eab5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -37,6 +37,7 @@ mod version_files; mod log_level; mod default_version; mod directories; +mod pretty_serde; fn main() { env_logger::init(); diff --git a/src/pretty_serde.rs b/src/pretty_serde.rs new file mode 100644 index 000000000..fc5e175e9 --- /dev/null +++ b/src/pretty_serde.rs @@ -0,0 +1,51 @@ +use miette::SourceOffset; + +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +#[error("malformed json\n{}", self.report())] +pub struct DecodeError { + cause: serde_json::Error, + #[source_code] + input: String, + #[label("at this position")] + location: SourceOffset, +} + +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +#[error("")] +pub struct ClonedError { + message: String, + #[source_code] + input: String, + #[label("{message}")] + location: SourceOffset, +} + +impl DecodeError { + pub fn from_serde(input: impl Into, cause: serde_json::Error) -> Self { + let input = input.into(); + let location = SourceOffset::from_location(&input, cause.line(), cause.column()); + DecodeError { + cause, + input, + location, + } + } + + pub fn report(&self) -> String { + use colored::Colorize; + let report = miette::Report::from(ClonedError { + message: self.cause.to_string().italic().to_string(), + input: self.input.clone(), + location: self.location, + }); + + let mut output = String::new(); + + for line in format!("{report:?}").lines().skip(1) { + use std::fmt::Write; + writeln!(&mut output, "{line}").unwrap(); + } + + output.white().to_string() + } +} diff --git a/src/remote_node_index.rs b/src/remote_node_index.rs index 644f87fba..1b343883c 100644 --- a/src/remote_node_index.rs +++ b/src/remote_node_index.rs @@ -1,4 +1,4 @@ -use crate::version::Version; +use crate::{pretty_serde::DecodeError, version::Version}; use serde::Deserialize; use url::Url; @@ -66,15 +66,31 @@ pub struct IndexedNodeVersion { // pub files: Vec, } +#[derive(Debug, thiserror::Error, miette::Diagnostic)] +pub enum Error { + #[error("can't get remote versions file: {0}")] + #[diagnostic(transparent)] + Http(#[from] crate::http::Error), + #[error("can't decode remote versions file: {0}")] + #[diagnostic(transparent)] + Decode(#[from] DecodeError), +} + /// Prints /// /// ```rust /// use crate::remote_node_index::list; /// ``` -pub fn list(base_url: &Url) -> Result, crate::http::Error> { +pub fn list(base_url: &Url) -> Result, Error> { + let base_url = base_url.as_str().trim_end_matches('/'); let index_json_url = format!("{base_url}/index.json"); - let resp = crate::http::get(&index_json_url)?; - let mut value: Vec = resp.json()?; + let resp = crate::http::get(&index_json_url) + .map_err(crate::http::Error::from)? + .error_for_status() + .map_err(crate::http::Error::from)?; + let text = resp.text().map_err(crate::http::Error::from)?; + let mut value: Vec = + serde_json::from_str(&text[..]).map_err(|cause| DecodeError::from_serde(text, cause))?; value.sort_by(|a, b| a.version.cmp(&b.version)); Ok(value) }