perseus/
errors.rs

1#![allow(missing_docs)]
2
3#[cfg(engine)]
4use crate::i18n::TranslationsManagerError;
5use thiserror::Error;
6
7/// All errors that can be returned from this crate.
8#[derive(Error, Debug)]
9pub enum Error {
10    #[error(transparent)]
11    ClientError(#[from] ClientError),
12    #[cfg(engine)]
13    #[error(transparent)]
14    ServerError(#[from] ServerError),
15    #[cfg(engine)]
16    #[error(transparent)]
17    EngineError(#[from] EngineError),
18    // Plugin errors could come from literally anywhere, and could have entirely arbitrary data
19    #[error(transparent)]
20    PluginError(#[from] PluginError),
21}
22
23#[derive(Error, Debug)]
24#[error("plugin '{name}' returned an error (this is unlikely to be Perseus' fault)")]
25pub struct PluginError {
26    pub name: String,
27    #[source]
28    pub source: Box<dyn std::error::Error + Send + Sync>,
29}
30
31/// Errors that can occur in the server-side engine system (responsible for
32/// building the app).
33#[cfg(engine)]
34#[derive(Error, Debug)]
35pub enum EngineError {
36    // Many of the build/export processes return these more generic errors
37    #[error(transparent)]
38    ServerError(#[from] ServerError),
39    #[error("couldn't copy static directory at '{path}' to '{dest}'")]
40    CopyStaticDirError {
41        #[source]
42        source: fs_extra::error::Error,
43        path: String,
44        dest: String,
45    },
46    #[error("couldn't copy static alias file from '{from}' to '{to}'")]
47    CopyStaticAliasFileError {
48        #[source]
49        source: std::io::Error,
50        from: String,
51        to: String,
52    },
53    #[error("couldn't copy static alias directory from '{from}' to '{to}'")]
54    CopyStaticAliasDirErr {
55        #[source]
56        source: fs_extra::error::Error,
57        from: String,
58        to: String,
59    },
60    #[error("couldn't write the generated error page to '{dest}'")]
61    WriteErrorPageError {
62        #[source]
63        source: std::io::Error,
64        dest: String,
65    },
66    #[error("couldn't create the parent directories needed for the nested static alias '{alias}'")]
67    NestedStaticAliasDirCreationFailed {
68        #[source]
69        source: std::io::Error,
70        alias: String,
71    },
72}
73
74/// Errors that can occur in the browser.
75///
76/// **Important:** any changes to this `enum` constitute a breaking change,
77/// since users match this in their error pages. Changes in underlying
78/// `enum`s are not considered breaking (e.g. introducing a new invariant
79/// error).
80///
81/// **Warning:** in all these cases, except `ClientError::ServerError`, the user
82/// can already see the prerendered version of the page, it just isn't
83/// interactive. Only in that case will your error page occupy the entire
84/// screen, otherwise it will be placed into a `div` with the class
85/// `__perseus-error`, a deliberate choice to reinforce the best practice of
86/// giving the user as much as possible (it might not be interactive, but they
87/// can still use a rudimentary version). See the book for further details.
88///
89/// # Panic handling
90/// In a rather unorthodox manner, Perseus will do its level best to get an
91/// error message to the user of your app, no matter what happens. For this
92/// reason, this `enum` includes a `Panic` variant that will be provided when a
93/// panic has been intercepted. In this case, your error view will be rendered
94/// with no reactor, translations, or anything else available to it. What you do
95/// at this time is extremely important, since any panics in the code that
96/// handles that variant **cannot be caught**, leaving the user with no error
97/// message and an app that has completely frozen.
98///
99/// The `Panic` variant on this type only provides a formatted panic message,
100/// and nothing else from [`std::panic::PanicInfo`], due to lifetime
101/// constraints. Since the message formatting is done by the standard library,
102/// which automatically takes account of the `payload` and `message`, the only
103/// other properties are `location` and `can_unwind`: the latter should be
104/// handled by Perseus if it ever is, and the former shoudl not be exposed to
105/// end users. Currently, there is no way to get the underlying `PanicInfo`
106/// through Perseus' error handling system (although a plugin could do it
107/// by overriding the panic handler, but this is usually a bad idea).
108#[derive(Error, Debug)]
109pub enum ClientError {
110    #[error("{0}")] // All formatted for us by `std`
111    Panic(String),
112    #[error(transparent)]
113    PluginError(#[from] PluginError),
114    #[error(transparent)]
115    InvariantError(#[from] ClientInvariantError),
116    #[error(transparent)]
117    ThawError(#[from] ClientThawError),
118    // Not like the `ServerError` in this file!
119    #[error("an error with HTTP status code '{status}' was returned by the server: '{message}'")]
120    ServerError {
121        status: u16,
122        // This has to have been serialized unfortunately
123        message: String,
124    },
125    #[error(transparent)]
126    FetchError(#[from] FetchError),
127    #[error(transparent)]
128    PlatformError(#[from] ClientPlatformError),
129    #[error(transparent)]
130    PreloadError(#[from] ClientPreloadError), /* #[error(transparent)]
131                                               * FetchError(#[from] FetchError),
132                                               * ,
133                                               * // If the user is using the template macros, this should never be emitted because we can
134                                               * // ensure that the generated state is valid
135                                               * #[error("tried to deserialize invalid state
136                                               * (it was not malformed, but the state was not
137                                               * of
138                                               * the declared type)")] StateInvalid {
139                                               *     #[source]
140                                               *     source: serde_json::Error,
141                                               * },
142                                               * #[error("server informed us that a valid
143                                               * locale was invald (this almost certainly
144                                               * requires
145                                               * a hard reload)")] ValidLocaleNotProvided {
146                                               * locale: String },
147                                               */
148}
149
150/// Errors that can occur in the browser from certain invariants not being
151/// upheld. These should be extremely rare, but, since we don't control what
152/// HTML the browser gets, we avoid panicking in these cases.
153///
154/// Note that some of these invariants may be broken by an app's own code, such
155/// as invalid global state downcasting.
156#[derive(Debug, Error)]
157pub enum ClientInvariantError {
158    #[error("the render configuration was not found, or was malformed")]
159    RenderCfg,
160    #[error("the global state was not found, or was malformed (even apps not using global state should have an empty one injected)")]
161    GlobalState,
162    // This won't be triggered for HSR
163    #[error("attempted to register state on a page/capsule that had been previously declared as having no state")]
164    IllegalStateRegistration,
165    #[error(
166        "attempted to downcast reactive global state to the incorrect type (this is an error)"
167    )]
168    GlobalStateDowncast,
169    // This is technically a typing error, but we do the typing internally, so this should be
170    // impossible
171    #[error("invalid page/widget state found")]
172    InvalidState {
173        #[source]
174        source: serde_json::Error,
175    },
176    // Invariant because the user would have had to call something like `.template_with_state()`
177    // for this to happen
178    #[error("no state was found for a page/widget that expected state (you might have forgotten to write a state generation function, like `get_build_state`)")]
179    NoState,
180    #[error("the initial state was not found, or was malformed")]
181    InitialState,
182    #[error("the initial state denoted an error, but this was malformed")]
183    InitialStateError {
184        #[source]
185        source: serde_json::Error,
186    },
187    #[error(
188        "the locale '{locale}', which is supported by this app, was not returned by the server"
189    )]
190    ValidLocaleNotProvided { locale: String },
191    // This is just for initial loads (`__PERSEUS_TRANSLATIONS` window variable)
192    #[error("the translations were not found, or were malformed (even apps not using i18n have a declaration of their lack of translations)")]
193    Translations,
194    #[error("we found the current page to be a 404, but the engine disagrees")]
195    RouterMismatch,
196    #[error("the widget states were not found, or were malformed (even pages not using widgets still have a declaration of these)")]
197    WidgetStates,
198    #[error("a widget was registered in the state store with only a head (but widgets do not have heads), implying a corruption")]
199    InvalidWidgetPssEntry,
200    #[error("the widget with path '{path}' was not found, indicating you are rendering an invalid widget on the browser-side only (you should refactor to always render the widget, but only have it do anything on the browser-side; that way, it can be verified on the engine-side, leading to errors at build-time rather than execution-time)")]
201    BadWidgetRouteMatch { path: String },
202}
203
204/// Errors that can occur as a result of user-instructed preloads. Note that
205/// this will not cover network-related errors, which are considered fetch
206/// errors (since they are likely not the fault of your code, whereas a
207/// `ClientPreloadError` probably is).
208#[derive(Debug, Error)]
209pub enum ClientPreloadError {
210    #[error("preloading '{path}' leads to a locale detection page, which implies a malformed url")]
211    PreloadLocaleDetection { path: String },
212    #[error("'{path}' was not found for preload")]
213    PreloadNotFound { path: String },
214}
215
216/// Errors that can occur in the browser while interfacing with browser
217/// functionality. These should never really occur unless you're operating in an
218/// extremely alien environment (which probably wouldn't support Wasm, but
219/// we try to allow maximal error page control).
220#[derive(Debug, Error)]
221pub enum ClientPlatformError {
222    #[error("failed to get current url for initial load determination")]
223    InitialPath,
224}
225
226/// Errors that can occur in the browser as a result of attempting to thaw
227/// provided state.
228#[derive(Debug, Error)]
229pub enum ClientThawError {
230    #[error("invalid frozen page/widget state")]
231    InvalidFrozenState {
232        #[source]
233        source: serde_json::Error,
234    },
235    #[error("invalid frozen global state")]
236    InvalidFrozenGlobalState {
237        #[source]
238        source: serde_json::Error,
239    },
240    #[error("this app uses global state, but the provided frozen state declared itself to have no global state")]
241    NoFrozenGlobalState,
242    #[error("invalid frozen app provided (this is likely a corruption)")]
243    InvalidFrozenApp {
244        #[source]
245        source: serde_json::Error,
246    },
247}
248
249/// Errors that can occur in the build process or while the server is running.
250#[cfg(engine)]
251#[derive(Error, Debug)]
252pub enum ServerError {
253    #[error("render function '{fn_name}' in template '{template_name}' failed (cause: {blame:?})")]
254    RenderFnFailed {
255        // This is something like `build_state`
256        fn_name: String,
257        template_name: String,
258        blame: ErrorBlame,
259        // This will be triggered by the user's custom render functions, which should be able to
260        // have any error type
261        #[source]
262        source: Box<dyn std::error::Error + Send + Sync>,
263    },
264    // We should only get a failure to minify if the user has given invalid HTML, or if Sycamore
265    // stuffed up somewhere
266    #[error("failed to minify html (you can disable the `minify` flag to avoid this; this is very likely a Sycamore bug, unless you've provided invalid custom HTML)")]
267    MinifyError {
268        #[source]
269        source: std::io::Error,
270    },
271    #[error("failed to decode url provided (probably malformed request)")]
272    UrlDecodeFailed {
273        #[source]
274        source: std::string::FromUtf8Error,
275    },
276    #[error("the template '{template_name}' had no helper build state written to the immutable store (the store has been tampered with, and the app must be rebuilt)")]
277    MissingBuildExtra { template_name: String },
278    #[error("the template '{template_name}' had invalid helper build state written to the immutable store (the store has been tampered with, and the app must be rebuilt)")]
279    InvalidBuildExtra {
280        template_name: String,
281        #[source]
282        source: serde_json::Error,
283    },
284    #[error("page state was encountered that could not be deserialized into serde_json::Value (the store has been tampered with, and the app must be rebuilt)")]
285    InvalidPageState {
286        #[source]
287        source: serde_json::Error,
288    },
289
290    // `PathWithoutLocale`
291    #[error("attempting to resolve dependency '{widget}' in locale '{locale}' produced a locale redirection verdict (this shouldn't be possible)")]
292    ResolveDepLocaleRedirection { widget: String, locale: String },
293    #[error("attempting to resolve dependency '{widget}' in locale '{locale}' produced a not found verdict (did you mistype the widget path?)")]
294    ResolveDepNotFound { widget: String, locale: String },
295
296    #[error("template '{template_name}' cannot be built at build-time due to one or more of its dependencies having state that may change later; to allow this template to be built later, add `.allow_rescheduling()` to your template definition")]
297    TemplateCannotBeRescheduled { template_name: String },
298    // This is a serious error in programming
299    #[error("a dependency tree was not resolved, but a function expecting it to have been was called (this is a server-side error)")]
300    DepTreeNotResolved,
301    #[error("the template name did not prefix the path (this request was severely malformed)")]
302    TemplateNameNotInPath,
303
304    #[error(transparent)]
305    StoreError(#[from] StoreError),
306    #[error(transparent)]
307    TranslationsManagerError(#[from] TranslationsManagerError),
308    #[error(transparent)]
309    BuildError(#[from] BuildError),
310    #[error(transparent)]
311    ExportError(#[from] ExportError),
312    #[error(transparent)]
313    ServeError(#[from] ServeError),
314    #[error(transparent)]
315    PluginError(#[from] PluginError),
316    // This can occur in state acquisition failures during prerendering
317    #[error(transparent)]
318    ClientError(#[from] ClientError),
319}
320/// Converts a server error into an HTTP status code.
321#[cfg(engine)]
322pub fn err_to_status_code(err: &ServerError) -> u16 {
323    match err {
324        ServerError::ServeError(ServeError::PageNotFound { .. }) => 404,
325        // Ambiguous (user-generated error), we'll rely on the given cause
326        ServerError::RenderFnFailed { blame, .. } => match blame {
327            ErrorBlame::Client(code) => code.unwrap_or(400),
328            ErrorBlame::Server(code) => code.unwrap_or(500),
329        },
330        // Any other errors go to a 500, they'll be misconfigurations or internal server errors
331        _ => 500,
332    }
333}
334
335/// Errors that can occur while reading from or writing to a mutable or
336/// immutable store.
337// We do need this on the client to complete some things
338#[derive(Error, Debug)]
339pub enum StoreError {
340    #[error("asset '{name}' not found in store")]
341    NotFound { name: String },
342    #[error("asset '{name}' couldn't be read from store")]
343    ReadFailed {
344        name: String,
345        #[source]
346        source: Box<dyn std::error::Error + Send + Sync>,
347    },
348    #[error("asset '{name}' couldn't be written to store")]
349    WriteFailed {
350        name: String,
351        #[source]
352        source: Box<dyn std::error::Error + Send + Sync>,
353    },
354}
355
356/// Errors that can occur while fetching a resource from the server.
357#[derive(Error, Debug)]
358pub enum FetchError {
359    #[error("asset of type '{ty}' fetched from '{url}' wasn't a string")]
360    NotString { url: String, ty: AssetType },
361    #[error(
362        "asset of type '{ty}' fetched from '{url}' returned status code '{status}' (expected 200)"
363    )]
364    NotOk {
365        url: String,
366        status: u16,
367        // The underlying body of the HTTP error response
368        err: String,
369        ty: AssetType,
370    },
371    #[error("asset of type '{ty}' fetched from '{url}' couldn't be serialized")]
372    SerFailed {
373        url: String,
374        #[source]
375        source: Box<dyn std::error::Error + Send + Sync>,
376        ty: AssetType,
377    },
378    /// This converts from a `JsValue` or the like.
379    #[error("the following error occurred while interfacing with JavaScript: {0}")]
380    Js(String),
381}
382
383/// The type of an asset fetched from the server. This allows distinguishing
384/// between errors in fetching, say, pages, vs. translations, which you may wish
385/// to handle differently.
386#[derive(Debug, Clone, Copy)]
387pub enum AssetType {
388    /// A page in the app.
389    Page,
390    /// A widget in the app.
391    Widget,
392    /// Translations for a locale.
393    Translations,
394    /// A page/widget the user asked to have preloaded.
395    Preload,
396}
397impl std::fmt::Display for AssetType {
398    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
399        write!(f, "{:?}", self)
400    }
401}
402
403/// Errors that can occur while building an app.
404#[cfg(engine)]
405#[derive(Error, Debug)]
406pub enum BuildError {
407    #[error("template '{template_name}' is missing feature '{feature_name}' (required due to its properties)")]
408    TemplateFeatureNotEnabled {
409        template_name: String,
410        feature_name: String,
411    },
412    #[error("html shell couldn't be found at '{path}'")]
413    HtmlShellNotFound {
414        path: String,
415        #[source]
416        source: std::io::Error,
417    },
418    #[error(
419        "invalid indicator '{indicator}' in time string (must be one of: s, m, h, d, w, M, y)"
420    )]
421    InvalidDatetimeIntervalIndicator { indicator: String },
422    #[error("asset 'render_cfg.json' invalid or corrupted (try cleaning all assets)")]
423    RenderCfgInvalid {
424        #[source]
425        source: serde_json::Error,
426    },
427}
428
429/// Errors that can occur while exporting an app to static files.
430#[cfg(engine)]
431#[derive(Error, Debug)]
432pub enum ExportError {
433    #[error("template '{template_name}' can't be exported because it depends on strategies that can't be run at build-time (only build state and build paths can be used in exportable templates)")]
434    TemplateNotExportable { template_name: String },
435    #[error("template '{template_name}' wasn't found in built artifacts (run `perseus clean --dist` if this persists)")]
436    TemplateNotFound { template_name: String },
437    #[error("your app can't be exported because its global state depends on strategies that can't be run at build time (only build state can be used in exportable apps)")]
438    GlobalStateNotExportable,
439    #[error("template '{template_name} can't be exported because one or more of its widget dependencies use state generation strategies that can't be run at build-time")]
440    DependenciesNotExportable { template_name: String },
441    // This is used in error page exports
442    #[error("invalid status code provided for error page export (please provide a valid http status code)")]
443    InvalidStatusCode,
444}
445
446/// Errors that can occur while serving an app. These are integration-agnostic.
447#[derive(Error, Debug)]
448pub enum ServeError {
449    #[error("page/widget at '{path}' not found")]
450    PageNotFound { path: String },
451    #[error("both build and request states were defined for a template when only one or fewer were expected (should it be able to amalgamate states?)")]
452    BothStatesDefined,
453    #[cfg(engine)]
454    #[error("couldn't parse revalidation datetime (try cleaning all assets)")]
455    BadRevalidate {
456        #[source]
457        source: chrono::ParseError,
458    },
459}
460
461/// Defines who caused an ambiguous error message so we can reliably create an
462/// HTTP status code. Specific status codes may be provided in either case, or
463/// the defaults (400 for client, 500 for server) will be used.
464///
465/// The default implementation will produce a server-blamed 500 error.
466#[derive(Debug)]
467pub enum ErrorBlame {
468    Client(Option<u16>),
469    Server(Option<u16>),
470}
471impl Default for ErrorBlame {
472    fn default() -> Self {
473        Self::Server(None)
474    }
475}
476
477/// An error that has an attached cause that blames either the client or the
478/// server for its occurrence. You can convert any error into this with
479/// `.into()` or `?`, which will set the cause to the server by default,
480/// resulting in a *500 Internal Server Error* HTTP status code. If this isn't
481/// what you want, you'll need to initialize this explicitly.
482///
483/// *Note for those using `anyhow`: use `.map_err(|e| anyhow::anyhow!(e))?`
484/// to use anyhow in Perseus render functions.*
485#[cfg(engine)]
486#[derive(Debug)]
487pub struct BlamedError<E: Send + Sync> {
488    /// The underlying error.
489    pub error: E,
490    /// Who is to blame for the error.
491    pub blame: ErrorBlame,
492}
493#[cfg(engine)]
494impl<E: Into<Box<dyn std::error::Error + Send + Sync + 'static>> + Send + Sync> BlamedError<E> {
495    /// Converts this blamed error into an internal boxed version that is
496    /// generic over the error type.
497    pub(crate) fn into_boxed(self) -> GenericBlamedError {
498        BlamedError {
499            error: self.error.into(),
500            blame: self.blame,
501        }
502    }
503}
504// We should be able to convert any error into this easily (e.g. with `?`) with
505// the default being to blame the server
506#[cfg(engine)]
507impl<E: Into<Box<dyn std::error::Error + Send + Sync + 'static>> + Send + Sync> From<E>
508    for BlamedError<E>
509{
510    fn from(error: E) -> Self {
511        Self {
512            error,
513            blame: ErrorBlame::default(),
514        }
515    }
516}
517
518/// A simple wrapper for generic, boxed, blamed errors.
519#[cfg(engine)]
520pub(crate) type GenericBlamedError = BlamedError<Box<dyn std::error::Error + Send + Sync>>;