Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package asciinema for openSUSE:Factory checked in at 2026-06-19 16:35:39 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/asciinema (Old) and /work/SRC/openSUSE:Factory/.asciinema.new.1956 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "asciinema" Fri Jun 19 16:35:39 2026 rev:15 rq:1360388 version:3.2.1 Changes: -------- --- /work/SRC/openSUSE:Factory/asciinema/asciinema.changes 2026-05-12 19:27:04.452096517 +0200 +++ /work/SRC/openSUSE:Factory/.asciinema.new.1956/asciinema.changes 2026-06-19 17:13:33.073893585 +0200 @@ -1,0 +2,10 @@ +Thu Jun 18 20:06:25 UTC 2026 - Andreas Stieger <[email protected]> + +- Update to version 3.2.1: + * Improve error reporting for server API failures - server- + provided error messages are now surfaced, with actionable + guidance (e.g. running asciinema auth) + * Upgrade the virtual terminal (avt) to the latest version + * Upgrade dependencies, including security fixes + +------------------------------------------------------------------- Old: ---- asciinema-3.2.0.tar.zst New: ---- asciinema-3.2.1.tar.zst ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ asciinema.spec ++++++ --- /var/tmp/diff_new_pack.ARsG4P/_old 2026-06-19 17:13:39.330107604 +0200 +++ /var/tmp/diff_new_pack.ARsG4P/_new 2026-06-19 17:13:39.334107741 +0200 @@ -18,7 +18,7 @@ Name: asciinema -Version: 3.2.0 +Version: 3.2.1 Release: 0 Summary: Terminal session recorder License: GPL-3.0-or-later ++++++ _service ++++++ --- /var/tmp/diff_new_pack.ARsG4P/_old 2026-06-19 17:13:39.394109793 +0200 +++ /var/tmp/diff_new_pack.ARsG4P/_new 2026-06-19 17:13:39.402110067 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/asciinema/asciinema.git</param> <param name="versionformat">@PARENT_TAG@</param> <param name="scm">git</param> - <param name="revision">v3.2.0</param> + <param name="revision">v3.2.1</param> <param name="match-tag">*</param> <param name="versionrewrite-pattern">v(\d+\.\d+\.\d+)</param> <param name="versionrewrite-replacement">\1</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.ARsG4P/_old 2026-06-19 17:13:39.430111025 +0200 +++ /var/tmp/diff_new_pack.ARsG4P/_new 2026-06-19 17:13:39.434111162 +0200 @@ -1,7 +1,7 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/asciinema/asciinema.git</param> - <param name="changesrevision">202d5c5761687b489451e9bb1a5fe9189b73e9d9</param> + <param name="changesrevision">70c4af0505fe1dbc7a2170392559d258bd4af92c</param> </service> </servicedata> (No newline at EOF) ++++++ asciinema-3.2.0.tar.zst -> asciinema-3.2.1.tar.zst ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/.github/workflows/release.yml new/asciinema-3.2.1/.github/workflows/release.yml --- old/asciinema-3.2.0/.github/workflows/release.yml 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/.github/workflows/release.yml 2026-06-16 16:16:23.000000000 +0200 @@ -29,7 +29,7 @@ strategy: matrix: include: - - os: ubuntu-latest + - os: ubuntu-22.04 target: x86_64-unknown-linux-gnu use-cross: false diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/CHANGELOG.md new/asciinema-3.2.1/CHANGELOG.md --- old/asciinema-3.2.0/CHANGELOG.md 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/CHANGELOG.md 2026-06-16 16:16:23.000000000 +0200 @@ -1,5 +1,12 @@ # asciinema changelog +## 3.2.1 (2026-06-16) + +* Improved error reporting for server API failures - server-provided error messages are now surfaced, with actionable guidance (e.g. running `asciinema auth`) +* Built release binaries on Ubuntu 22.04 for broader compatibility with older Linux systems (#742) +* Upgraded the virtual terminal (avt) to the latest version +* Upgraded dependencies, including security fixes + ## 3.2.0 (2026-03-01) * Improved querying for terminal theme and version diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/Cargo.lock new/asciinema-3.2.1/Cargo.lock --- old/asciinema-3.2.0/Cargo.lock 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/Cargo.lock 2026-06-16 16:16:23.000000000 +0200 @@ -84,7 +84,7 @@ [[package]] name = "asciinema" -version = "3.2.0" +version = "3.2.1" dependencies = [ "anyhow", "async-trait", @@ -146,9 +146,9 @@ [[package]] name = "avt" -version = "0.17.0" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa0f99f7bcce0e99d842c94947f8d0ab5f6f3abc08424e1a4b58a8a7ae30f7c7" +checksum = "7179c44abe2ac36173d4713bfed24136e5988f005c7fe2c4fcde621d3d4d29b9" dependencies = [ "rgb", "unicode-width 0.1.14", @@ -947,9 +947,9 @@ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098" dependencies = [ "bytes", "getrandom 0.3.3", @@ -997,9 +997,9 @@ [[package]] name = "rand" -version = "0.9.1" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core", @@ -1211,9 +1211,9 @@ [[package]] name = "rustls-webpki" -version = "0.103.7" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10b3f4191e8a80e6b43eebabfac91e5dcecebb27a71f04e820c47ec41d314bf" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "ring", "rustls-pki-types", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/Cargo.toml new/asciinema-3.2.1/Cargo.toml --- old/asciinema-3.2.0/Cargo.toml 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/Cargo.toml 2026-06-16 16:16:23.000000000 +0200 @@ -1,6 +1,6 @@ [package] name = "asciinema" -version = "3.2.0" +version = "3.2.1" edition = "2021" authors = ["Marcin Kulik <[email protected]>"] homepage = "https://asciinema.org" @@ -24,7 +24,7 @@ config = { version = "0.15", default-features = false, features = ["toml"] } which = "8.0" tempfile = "3.23" -avt = "0.17" +avt = "0.18" axum = { version = "0.8", default-features = false, features = ["http1", "ws"] } tokio = { version = "1.40", features = ["rt-multi-thread", "net", "sync", "time", "fs", "process"] } futures-util = { version = "0.3", default-features = false, features = ["sink"] } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/README.md new/asciinema-3.2.1/README.md --- old/asciinema-3.2.0/README.md 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/README.md 2026-06-16 16:16:23.000000000 +0200 @@ -21,21 +21,45 @@ asciinema runs on GNU/Linux, macOS and FreeBSD. -<a href="https://asciinema.org/a/756853?autoplay=1"><img src="https://asciinema.org/a/756853.svg" alt="asciinema CLI demo" width="100%" /></a> +<a href="https://asciinema.org/a/756853"><img src="https://asciinema.org/a/756853.svg" alt="asciinema CLI demo" width="100%" /></a> Notable features: -- recording and replaying of sessions inside a terminal, +- recording of terminal sessions to a file, with optional [keyboard input + capture](https://docs.asciinema.org/manual/cli/quick-start/) and configurable + environment variable capture, +- replaying of recordings inside a terminal, with adjustable speed, looping, + idle time limiting, step-by-step navigation, + pause-on-[markers](https://docs.asciinema.org/manual/cli/markers/), and + optional terminal auto-resize, - local and remote [live streaming](https://docs.asciinema.org/manual/cli/quick-start/#stream-a-terminal-session) - of terminal sessions to multiple viewers in real-time, -- [lightweight recording - format](https://docs.asciinema.org/manual/asciicast/v3/), which is highly - compressible (down to 15% of the original size e.g. with `zstd` or `gzip`), + of terminal sessions to multiple viewers in real-time, including a built-in + HTTP server with an embedded web player for LAN/localhost viewing, +- combined sessions: record to a file while streaming locally and remotely at + the same time, +- [lightweight asciicast recording + format](https://docs.asciinema.org/manual/asciicast/v3/), highly compressible + (8% of the original size on average), +- conversion from asciicast v1/v2/v3 to asciicast v2/v3, raw terminal output, + or plain text, +- concatenation of multiple recordings into one, with timing adjusted + automatically, +- mid-session controls: pause/resume capture and add markers on the fly via + [customizable key bindings](https://docs.asciinema.org/manual/cli/configuration/), +- session metadata capture, including terminal size, terminal theme, command, + and title, +- configuration file support for defaults such as recording command, capture + options, playback speed, idle time limit, notifications, and key bindings, +- headless mode, configurable terminal window size, and exit-status propagation + for scripted and CI-friendly recording and streaming, +- support for stdin/stdout in conversion and playback from local files, stdin, + or HTTP(S) URLs, - integration with [asciinema server](https://docs.asciinema.org/manual/server/), e.g. - [asciinema.org](https://asciinema.org), for easy recording hosting and live - streaming. + [asciinema.org](https://asciinema.org), for uploads, hosting, remote live + streaming, self-hosted servers, visibility control, descriptions, and + synchronized audio URLs. To record a session run this command in your shell: diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/default.nix new/asciinema-3.2.1/default.nix --- old/asciinema-3.2.0/default.nix 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/default.nix 2026-06-16 16:16:23.000000000 +0200 @@ -24,7 +24,7 @@ cargoLock.lockFile = ./Cargo.lock; nativeBuildInputs = [ rust ]; - buildInputs = lib.optional stdenv.isDarwin [ + buildInputs = lib.optionals stdenv.isDarwin [ libiconv ]; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/flake.lock new/asciinema-3.2.1/flake.lock --- old/asciinema-3.2.0/flake.lock 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/flake.lock 2026-06-16 16:16:23.000000000 +0200 @@ -20,11 +20,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772198003, - "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", + "lastModified": 1781074563, + "narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", + "rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca", "type": "github" }, "original": { @@ -34,22 +34,6 @@ "type": "github" } }, - "nixpkgs_2": { - "locked": { - "lastModified": 1744536153, - "narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11", - "type": "github" - }, - "original": { - "owner": "NixOS", - "ref": "nixpkgs-unstable", - "repo": "nixpkgs", - "type": "github" - } - }, "root": { "inputs": { "flake-utils": "flake-utils", @@ -59,14 +43,16 @@ }, "rust-overlay": { "inputs": { - "nixpkgs": "nixpkgs_2" + "nixpkgs": [ + "nixpkgs" + ] }, "locked": { - "lastModified": 1772334676, - "narHash": "sha256-Jrc0J3AH+iNJDlUze3+FJZv2R0BZnhANFnD52V4kyvI=", + "lastModified": 1781580018, + "narHash": "sha256-BlTedbM77FmesD2ZqR73vhFy+y77UrhefV7IYw1pDsk=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "9879be11f30fd3bbf848e653a7f991549e8973b5", + "rev": "8bceba21a1ebea535c27c4dc723a0d5a4db9e386", "type": "github" }, "original": { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/flake.nix new/asciinema-3.2.1/flake.nix --- old/asciinema-3.2.0/flake.nix 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/flake.nix 2026-06-16 16:16:23.000000000 +0200 @@ -4,6 +4,7 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay.url = "github:oxalica/rust-overlay"; + rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; flake-utils.url = "github:numtide/flake-utils"; }; @@ -22,7 +23,7 @@ overlays = [ (import rust-overlay) ]; }; - packageToml = (builtins.fromTOML (builtins.readFile ./Cargo.toml)).package; + packageToml = (fromTOML (builtins.readFile ./Cargo.toml)).package; msrv = packageToml.rust-version; in { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/shell.nix new/asciinema-3.2.1/shell.nix --- old/asciinema-3.2.0/shell.nix 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/shell.nix 2026-06-16 16:16:23.000000000 +0200 @@ -12,6 +12,7 @@ (package.override { rust = rust.override { extensions = [ + "rustfmt" "rust-src" "rust-analyzer" "clippy" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/src/api.rs new/asciinema-3.2.1/src/api.rs --- old/asciinema-3.2.0/src/api.rs 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/src/api.rs 2026-06-16 16:16:23.000000000 +0200 @@ -68,6 +68,15 @@ #[derive(Debug, Deserialize)] struct ErrorResponse { + #[serde(rename = "type")] + error_type: Option<String>, + message: Option<String>, + details: Option<serde_json::Value>, +} + +#[derive(Debug, Deserialize)] +struct ErrorDetail { + field: Option<String>, message: String, } @@ -91,19 +100,11 @@ .send() .await?; - if response.status().as_u16() == 413 { - match response.json::<ErrorResponse>().await { - Ok(json) => { - bail!("{}", json.message); - } + let legacy_fallback = (response.status().as_u16() == 413) + .then(|| "The recording exceeds the server-configured size limit".to_owned()); - Err(_) => { - bail!("The recording exceeds the server-configured size limit"); - } - } - } else { - response.error_for_status_ref()?; - } + let server_hostname = server_url.host().unwrap().to_string(); + let response = handle_response_status(response, &server_hostname, legacy_fallback).await?; Ok(response.json::<RecordingResponse>().await?) } @@ -235,29 +236,107 @@ response: Response, server_url: &Url, ) -> Result<T> { - let server_hostname = server_url.host().unwrap(); + let server_hostname = server_url.host().unwrap().to_string(); + + let legacy_fallback = match response.status().as_u16() { + 404 | 422 => Some(format!("{server_hostname} doesn't support streaming")), + _ => None, + }; + + let response = handle_response_status(response, &server_hostname, legacy_fallback).await?; + + response.json::<T>().await.map_err(|e| e.into()) +} + +async fn handle_response_status( + response: Response, + server_hostname: &str, + legacy_fallback: Option<String>, +) -> Result<Response> { + let status_error = match response.error_for_status_ref() { + Ok(_) => return Ok(response), + Err(error) => error, + }; + + let message = match response.bytes().await { + Ok(body) => parse_error_response(&body) + .and_then(|response| render_error_response(response, server_hostname)), + Err(_) => None, + }; + + if let Some(message) = message.or(legacy_fallback) { + bail!(message); + } + + Err(status_error.into()) +} - match response.status().as_u16() { - 401 => bail!( - "this CLI hasn't been authenticated with {server_hostname} - run `asciinema auth` first" - ), - - 404 => match response.json::<ErrorResponse>().await { - Ok(json) => bail!("{}", json.message), - Err(_) => bail!("{server_hostname} doesn't support streaming"), - }, - - 422 => match response.json::<ErrorResponse>().await { - Ok(json) => bail!("{}", json.message), - Err(_) => bail!("{server_hostname} doesn't support streaming"), - }, +fn parse_error_response(body: &[u8]) -> Option<ErrorResponse> { + serde_json::from_slice(body).ok() +} + +fn render_error_response(response: ErrorResponse, server_hostname: &str) -> Option<String> { + let guidance = match response.error_type.as_deref() { + Some("account_required") => Some(format!( + "Run `asciinema auth` to link this CLI to your {server_hostname} account." + )), - _ => { - response.error_for_status_ref()?; + Some("upload_limit_reached") => { + Some("Run `asciinema auth` and follow the link to upload more.".to_owned()) } + + _ => None, + }; + + let mut message = format_error_response(response)?; + + if let Some(guidance) = guidance { + message.push_str("\n\n"); + message.push_str(&guidance); } - response.json::<T>().await.map_err(|e| e.into()) + Some(message) +} + +fn format_error_response(response: ErrorResponse) -> Option<String> { + let mut message = response + .message + .filter(|message| !message.trim().is_empty())?; + + if response.error_type.as_deref() != Some("validation_failed") { + return Some(message); + } + + if let Some(serde_json::Value::Array(details)) = response.details { + let mut has_details = false; + + for value in details { + let Ok(detail) = serde_json::from_value::<ErrorDetail>(value) else { + continue; + }; + + if detail.message.trim().is_empty() { + continue; + } + + if !has_details { + if !message.ends_with(':') { + message.push(':'); + } + + has_details = true; + } + + match detail.field.as_deref().map(str::trim) { + Some(field) if !field.is_empty() && field != "." => { + message.push_str(&format!("\n {field}: {}", detail.message)); + } + _ => message.push_str(&format!("\n {}", detail.message)), + } + } + } + + Some(message) } fn add_headers(builder: RequestBuilder, install_id: &str) -> RequestBuilder { @@ -281,3 +360,229 @@ ua.to_owned() } + +#[cfg(test)] +mod tests { + use axum::http::{Response as HttpResponse, StatusCode}; + use tokio::runtime::Runtime; + + use super::{ + format_error_response, handle_response_status, parse_error_response, render_error_response, + }; + + const SERVER_HOSTNAME: &str = "example.com"; + + fn response(status: StatusCode, body: &'static str) -> reqwest::Response { + HttpResponse::builder() + .status(status) + .body(body) + .unwrap() + .into() + } + + fn parse_error_message(body: &[u8]) -> Option<String> { + parse_error_response(body).and_then(format_error_response) + } + + fn render_error_message(body: &[u8]) -> Option<String> { + parse_error_response(body) + .and_then(|response| render_error_response(response, SERVER_HOSTNAME)) + } + + #[test] + fn augments_account_required_error() { + let body = br#"{ + "type": "account_required", + "message": "This action requires an account" + }"#; + + assert_eq!( + render_error_message(body), + Some( + "This action requires an account\n\n\ + Run `asciinema auth` to link this CLI to your example.com account." + .to_owned() + ) + ); + } + + #[test] + fn augments_upload_limit_reached_error() { + let body = br#"{ + "type": "upload_limit_reached", + "message": "Anonymous upload limit reached" + }"#; + + assert_eq!( + render_error_message(body), + Some( + "Anonymous upload limit reached\n\n\ + Run `asciinema auth` and follow the link to upload more." + .to_owned() + ) + ); + + let error = Runtime::new() + .unwrap() + .block_on(handle_response_status( + response(StatusCode::FORBIDDEN, std::str::from_utf8(body).unwrap()), + SERVER_HOSTNAME, + None, + )) + .unwrap_err(); + + assert_eq!( + error.to_string(), + "Anonymous upload limit reached\n\n\ + Run `asciinema auth` and follow the link to upload more." + ); + } + + #[test] + fn leaves_unauthenticated_error_verbatim() { + let body = br#"{ + "type": "unauthenticated", + "message": "Installation ID has been revoked" + }"#; + + assert_eq!( + render_error_message(body), + Some("Installation ID has been revoked".to_owned()) + ); + } + + #[test] + fn leaves_unknown_error_type_verbatim() { + let body = br#"{ + "type": "future_error", + "message": "A future server error" + }"#; + + assert_eq!( + render_error_message(body), + Some("A future server error".to_owned()) + ); + } + + #[test] + fn formats_validation_error_details() { + let body = br#"{ + "type": "validation_failed", + "message": "Validation failed", + "details": [ + { + "field": "audio_url", + "message": "has invalid format" + }, + { + "field": "audio_url", + "message": "should be at most 255 character(s)" + } + ] + }"#; + + assert_eq!( + render_error_message(body), + Some( + "Validation failed:\n audio_url: has invalid format\n \ + audio_url: should be at most 255 character(s)" + .to_owned() + ) + ); + } + + #[test] + fn ignores_invalid_validation_error_details() { + for body in [ + br#"{ + "type": "validation_failed", + "message": "Validation failed" + }"# + .as_slice(), + br#"{ + "type": "validation_failed", + "message": "Validation failed", + "details": "invalid" + }"# + .as_slice(), + br#"{ + "type": "validation_failed", + "message": "Validation failed", + "details": [ + {"field": "idle_time_limit"}, + {"field": "", "message": ""} + ] + }"# + .as_slice(), + ] { + assert_eq!( + parse_error_message(body), + Some("Validation failed".to_owned()) + ); + } + } + + #[test] + fn formats_fieldless_validation_error_details() { + let body = br#"{ + "type": "validation_failed", + "message": "Validation failed", + "details": [ + {"message": "recording metadata is invalid"}, + {"field": "", "message": "recording data is invalid"}, + {"field": " ", "message": "recording options are invalid"}, + {"field": ".", "message": "recording is invalid"} + ] + }"#; + + assert_eq!( + parse_error_message(body), + Some( + "Validation failed:\n recording metadata is invalid\n \ + recording data is invalid\n recording options are invalid\n recording is invalid" + .to_owned() + ) + ); + } + + #[test] + fn ignores_error_body_without_message() { + assert_eq!( + parse_error_message(br#"{"type":"upload_limit_reached"}"#), + None + ); + assert_eq!(parse_error_message(br#"{"message":""}"#), None); + } + + #[test] + fn falls_back_to_status_error_for_invalid_or_empty_body() { + assert_eq!(parse_error_message(b"not JSON"), None); + assert_eq!(parse_error_message(b""), None); + + for body in ["not JSON", ""] { + let error = Runtime::new() + .unwrap() + .block_on(handle_response_status( + response(StatusCode::UNPROCESSABLE_ENTITY, body), + SERVER_HOSTNAME, + Some("The server doesn't support streaming".to_owned()), + )) + .unwrap_err(); + + assert_eq!(error.to_string(), "The server doesn't support streaming"); + } + + for body in ["not JSON", ""] { + let error = Runtime::new() + .unwrap() + .block_on(handle_response_status( + response(StatusCode::FORBIDDEN, body), + SERVER_HOSTNAME, + None, + )) + .unwrap_err(); + + assert!(error.to_string().contains("403 Forbidden")); + } + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/asciinema-3.2.0/src/forwarder.rs new/asciinema-3.2.1/src/forwarder.rs --- old/asciinema-3.2.0/src/forwarder.rs 2026-03-01 16:23:16.000000000 +0100 +++ new/asciinema-3.2.1/src/forwarder.rs 2026-06-16 16:16:23.000000000 +0200 @@ -15,6 +15,7 @@ use tokio_tungstenite::tungstenite::protocol::CloseFrame; use tokio_tungstenite::tungstenite::{self, ClientRequestBuilder, Message}; use tokio_tungstenite::{MaybeTlsStream, WebSocketStream}; +use tokio_util::sync::CancellationToken; use tracing::{debug, error, info}; use crate::alis; @@ -22,17 +23,17 @@ use crate::notifier::Notifier; use crate::stream::{Event, Subscriber}; -const PING_INTERVAL: u64 = 15; -const PING_TIMEOUT: u64 = 10; -const SEND_TIMEOUT: u64 = 10; -const RECONNECT_DELAY_BASE: u64 = 500; -const RECONNECT_DELAY_CAP: u64 = 10_000; +const PING_INTERVAL: Duration = Duration::from_secs(15); +const PING_TIMEOUT: Duration = Duration::from_secs(10); +const SEND_TIMEOUT: Duration = Duration::from_secs(10); +const RECONNECT_DELAY_BASE_MS: u64 = 500; +const RECONNECT_DELAY_CAP_MS: u64 = 10_000; pub async fn forward<N: Notifier>( url: url::Url, subscriber: Subscriber, mut notifier: N, - shutdown_token: tokio_util::sync::CancellationToken, + shutdown_token: CancellationToken, ) -> anyhow::Result<()> { info!("forwarding to {url}"); let mut reconnect_attempt = 0; @@ -192,7 +193,7 @@ ping = pings.next() => { send_with_timeout(&mut sink, ping.unwrap()).await??; - ping_timeout = Box::pin(time::sleep(Duration::from_secs(PING_TIMEOUT))); + ping_timeout = Box::pin(time::sleep(PING_TIMEOUT)); } _ = &mut ping_timeout => bail!("ping timeout"), @@ -223,7 +224,7 @@ sink: &mut SplitSink<WebSocketStream<MaybeTlsStream<TcpStream>>, Message>, message: Message, ) -> anyhow::Result<Result<(), tungstenite::Error>> { - time::timeout(Duration::from_secs(SEND_TIMEOUT), sink.send(message)) + time::timeout(SEND_TIMEOUT, sink.send(message)) .await .map_err(|_| anyhow!("send timeout")) } @@ -249,7 +250,7 @@ fn exponential_delay(attempt: usize) -> u64 { let mut rng = rand::rng(); let attempt = attempt.min(10); - let exp = (RECONNECT_DELAY_BASE * 2_u64.pow(attempt as u32)).min(RECONNECT_DELAY_CAP); + let exp = (RECONNECT_DELAY_BASE_MS * 2_u64.pow(attempt as u32)).min(RECONNECT_DELAY_CAP_MS); rng.random_range((exp / 2)..exp) } @@ -269,7 +270,7 @@ } fn ping_stream() -> impl Stream<Item = Message> { - IntervalStream::new(time::interval(Duration::from_secs(PING_INTERVAL))) + IntervalStream::new(time::interval(PING_INTERVAL)) .skip(1) .map(|_| Message::Ping(vec![].into())) } ++++++ asciinema.obsinfo ++++++ --- /var/tmp/diff_new_pack.ARsG4P/_old 2026-06-19 17:13:39.686119783 +0200 +++ /var/tmp/diff_new_pack.ARsG4P/_new 2026-06-19 17:13:39.690119920 +0200 @@ -1,5 +1,5 @@ name: asciinema -version: 3.2.0 -mtime: 1772378596 -commit: 202d5c5761687b489451e9bb1a5fe9189b73e9d9 +version: 3.2.1 +mtime: 1781619383 +commit: 70c4af0505fe1dbc7a2170392559d258bd4af92c ++++++ vendor.tar.zst ++++++ /work/SRC/openSUSE:Factory/asciinema/vendor.tar.zst /work/SRC/openSUSE:Factory/.asciinema.new.1956/vendor.tar.zst differ: char 7, line 1
