Skip to main content

webui_desktop/
error.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT license.
3
4use std::path::PathBuf;
5
6use thiserror::Error;
7
8/// Result type used by the WebUI desktop runtime.
9pub type Result<T> = std::result::Result<T, DesktopError>;
10
11/// Failure to establish an app-owned persistent browser-data boundary.
12#[derive(Debug, Error, PartialEq, Eq)]
13pub enum WebsiteDataError {
14    /// Persistence requires an explicit identity.
15    #[error("persistent website data requires an app_id; help: set LocalServerAppBuilder::app_id to the packaged application's bundle identifier")]
16    MissingIdentity,
17    /// The identity cannot safely name app-owned storage.
18    #[error("invalid app_id for persistent website data; help: use 1–255 ASCII letters, digits, dots, underscores or hyphens, with no trailing dot")]
19    InvalidIdentity,
20    /// The process is not running as the executable of a real macOS .app bundle.
21    #[error("persistent website data requires a packaged macOS .app executable; help: launch the app from its packaged Contents/MacOS executable")]
22    Unpackaged,
23    /// The requested identity does not own the running application bundle.
24    #[error("app_id does not match the running macOS bundle identifier; help: use the exact CFBundleIdentifier of the packaged .app")]
25    IdentityMismatch,
26    /// No safe native-owned persistent store is available on this platform.
27    #[error("persistent website data is unsupported on this platform; help: use a packaged macOS app or a Windows app-owned WebView2 profile")]
28    Unsupported,
29}
30
31/// Errors produced by the runtime-neutral WebUI desktop layer.
32#[derive(Debug, Error)]
33pub enum DesktopError {
34    /// An explicit native website-data persistence request failed.
35    #[error(transparent)]
36    WebsiteData(#[from] WebsiteDataError),
37    /// Application IPC configuration or delivery failed.
38    #[cfg(feature = "application-ipc")]
39    #[error("desktop application IPC failed")]
40    Ipc(#[from] crate::ipc::IpcError),
41
42    /// No desktop bundle could be found beside the running executable.
43    #[error("packaged desktop resources were not found")]
44    PackagedResourcesNotFound,
45
46    /// A native or custom desktop backend could not complete an operation.
47    #[error("desktop backend failed")]
48    Backend {
49        /// Underlying backend failure.
50        #[source]
51        source: Box<dyn std::error::Error + Send + Sync>,
52    },
53
54    /// I/O failure while reading or writing desktop runtime data.
55    #[error("I/O error while {context}")]
56    Io {
57        /// Operation that failed.
58        context: String,
59        /// Underlying I/O error.
60        #[source]
61        source: std::io::Error,
62    },
63
64    /// WebUI source build failed.
65    #[cfg(feature = "source")]
66    #[error("desktop WebUI build failed")]
67    Build(#[from] webui::WebUIError),
68
69    /// Handler rendering failed.
70    #[error("desktop render failed")]
71    Render(#[from] webui_handler::HandlerError),
72
73    /// Protocol serialization or deserialization failed.
74    #[error("desktop protocol error")]
75    Protocol(#[from] webui_protocol::ProtocolError),
76
77    /// State JSON could not be parsed.
78    #[error("failed to parse desktop state JSON from {}", path.display())]
79    StateJson {
80        /// State file path.
81        path: PathBuf,
82        /// Underlying JSON parser error.
83        #[source]
84        source: serde_json::Error,
85    },
86
87    /// JSON serialization failed.
88    #[error("failed while {context}")]
89    Serialization {
90        /// Operation that failed.
91        context: String,
92        /// Underlying JSON error.
93        #[source]
94        source: serde_json::Error,
95    },
96
97    /// Design token loading or resolution failed.
98    #[error("failed while {context}")]
99    Token {
100        /// Operation that failed.
101        context: String,
102        /// Underlying token error.
103        #[source]
104        source: webui_tokens::TokenError,
105    },
106
107    /// A custom-protocol asset path was unsafe.
108    #[error("invalid desktop asset path: {path}")]
109    InvalidAssetPath {
110        /// Rejected request path.
111        path: String,
112    },
113
114    /// A Rust route provider pattern is invalid.
115    #[error("invalid desktop route pattern: {pattern}")]
116    InvalidRoutePattern {
117        /// Rejected route pattern.
118        pattern: String,
119        /// Actionable fix.
120        help: String,
121    },
122
123    /// A Rust route provider failed.
124    #[error("desktop route provider failed for {path}: {message}")]
125    RouteProvider {
126        /// Request path.
127        path: String,
128        /// Provider error message.
129        message: String,
130    },
131
132    /// An asset exceeds the configured in-memory response cap.
133    #[error(
134        "desktop asset is too large: {} ({} bytes, max {} bytes)",
135        path.display(),
136        size,
137        max_bytes
138    )]
139    AssetTooLarge {
140        /// Asset path.
141        path: PathBuf,
142        /// Actual size in bytes.
143        size: u64,
144        /// Configured maximum size.
145        max_bytes: u64,
146    },
147
148    /// Two bundle inputs resolve to the same output asset path.
149    #[error("desktop bundle asset collision: {path}")]
150    BundleAssetCollision {
151        /// Colliding bundle-relative path.
152        path: String,
153    },
154
155    /// A bundle or package output path overlaps an input path.
156    #[error(
157        "desktop output path {} overlaps {input_label} path {}",
158        output.display(),
159        input.display()
160    )]
161    OutputPathOverlap {
162        /// Output path that would be cleaned or written.
163        output: PathBuf,
164        /// Input path that must be preserved.
165        input: PathBuf,
166        /// Human-readable input label.
167        input_label: &'static str,
168    },
169
170    /// An already-compiled host package input is invalid or unsafe.
171    #[error("invalid precompiled desktop package: {message}; help: {help}")]
172    PackageValidation {
173        /// Specific rejected input.
174        message: String,
175        /// Actionable correction.
176        help: &'static str,
177    },
178
179    /// Bundle manifest serialization failed.
180    #[error("failed to serialize desktop bundle manifest")]
181    ManifestSerialization(#[source] serde_json::Error),
182
183    /// Bundle manifest deserialization failed.
184    #[error("failed to parse desktop bundle manifest at {}", path.display())]
185    ManifestDeserialization {
186        /// Manifest path.
187        path: PathBuf,
188        /// Underlying parser error.
189        #[source]
190        source: serde_json::Error,
191    },
192
193    /// Platform runtime does not support a required desktop capability.
194    #[error("unsupported desktop runtime: {message}")]
195    UnsupportedRuntime {
196        /// What capability is missing.
197        message: String,
198        /// Actionable fix.
199        help: String,
200    },
201}
202
203impl DesktopError {
204    /// Return an actionable hint for errors that have a clear next step.
205    #[must_use]
206    pub fn hint(&self) -> Option<&str> {
207        match self {
208            DesktopError::PackagedResourcesNotFound => Some(
209                "Run the app from its packaged bundle, or enable the source feature for development with cargo run --features source",
210            ),
211            DesktopError::InvalidAssetPath { .. } => {
212                Some("Use a relative asset path without '.', '..', backslashes, NUL bytes, or drive prefixes")
213            }
214            DesktopError::AssetTooLarge { .. } => {
215                Some("Reduce the asset size or raise the desktop max asset size for a trusted app")
216            }
217            DesktopError::InvalidRoutePattern { help, .. } => Some(help.as_str()),
218            DesktopError::RouteProvider { .. } => {
219                Some("Fix the Rust route provider or return a valid route-scoped state object")
220            }
221            DesktopError::BundleAssetCollision { .. } => {
222                Some("Rename the static asset or generated CSS file so each bundle path is unique")
223            }
224            DesktopError::OutputPathOverlap { .. } => {
225                Some("Choose an output directory outside the app, bundle, state, asset, and runner paths")
226            }
227            DesktopError::PackageValidation { help, .. } => Some(help),
228            DesktopError::UnsupportedRuntime { help, .. } => Some(help.as_str()),
229            _ => None,
230        }
231    }
232
233    /// Return a flattened source-chain message for hosts that do not walk
234    /// [`std::error::Error::source`].
235    #[must_use]
236    pub fn chain_message(&self) -> String {
237        let mut message = self.to_string();
238        let mut source = std::error::Error::source(self);
239        while let Some(err) = source {
240            message.push_str(": ");
241            message.push_str(&err.to_string());
242            source = err.source();
243        }
244        message
245    }
246}
247
248#[cfg(all(test, feature = "application-ipc"))]
249mod tests {
250    use super::DesktopError;
251    use crate::ipc::{IpcError, IpcErrorCode};
252
253    #[test]
254    fn ipc_errors_preserve_their_typed_source_and_actionable_chain() {
255        let error = DesktopError::from(IpcError::new(
256            IpcErrorCode::PayloadTooLarge,
257            "frame exceeds the configured limit",
258            "send a smaller message",
259        ));
260        assert!(matches!(
261            &error,
262            DesktopError::Ipc(source) if source.code == IpcErrorCode::PayloadTooLarge
263        ));
264        assert!(std::error::Error::source(&error).is_some());
265        assert_eq!(
266            error.chain_message(),
267            "desktop application IPC failed: frame exceeds the configured limit; help: send a smaller message"
268        );
269    }
270}