diff --git a/Cargo.lock b/Cargo.lock index c9e11c8..a0cf625 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,21 @@ version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74f37166d7d48a0284b99dd824694c26119c700b53bf0d1540cdb147dbdaaf13" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "cgp" version = "0.2.0" @@ -159,6 +174,7 @@ dependencies = [ "itertools", "serde", "serde_json", + "sha1", ] [[package]] @@ -190,6 +206,25 @@ dependencies = [ "cgp-component", ] +[[package]] +name = "cpufeatures" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3" +dependencies = [ + "libc", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "datetime" version = "0.5.2" @@ -204,12 +239,32 @@ dependencies = [ "winapi", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check 0.9.5", +] + [[package]] name = "iso8601" version = "0.3.0" @@ -262,7 +317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" dependencies = [ "memchr", - "version_check", + "version_check 0.1.5", ] [[package]] @@ -346,6 +401,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "syn" version = "2.0.87" @@ -357,6 +423,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "unicode-ident" version = "1.0.13" @@ -375,6 +447,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 236cf16..fe85093 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,5 @@ serde = {version = "1", features = ["derive"] } itertools = "0.11.0" serde_json = "1" anyhow = "1" -datetime = "0.5.2" \ No newline at end of file +datetime = "0.5.2" +sha1 = "0.10.6" \ No newline at end of file diff --git a/content/SUMMARY.md b/content/SUMMARY.md index 7ff3f9c..1cd1d0e 100644 --- a/content/SUMMARY.md +++ b/content/SUMMARY.md @@ -22,8 +22,9 @@ - [Associated Types](associated-types.md) - [Error Handling](error-handling.md) - - [Delegated Error Raiser](delegated-error-raiser.md) - - [Detailed Error Reporting]() + - [Delegated Error Raisers](delegated-error-raiser.md) + - [Error Reporting](error-reporting.md) + - [Wrapping Errors]() - [Component Presets]() - [Trait-Generic Providers]() - [`WithProvider`]() diff --git a/content/delegated-error-raiser.md b/content/delegated-error-raiser.md index e708afb..432ea29 100644 --- a/content/delegated-error-raiser.md +++ b/content/delegated-error-raiser.md @@ -1,4 +1,4 @@ -# Delegated Error Raiser +# Delegated Error Raisers In the previous chapter, we have defined context-generic error raisers like `RaiseFrom` and `DebugAsAnyhow`, which can be use to raise any source error that satisfy certain @@ -13,7 +13,7 @@ For example, we may want to use `RaiseFrom` when there is a `From` instance, and In this chapter, we will cover the `UseDelegate` pattern, which offers a declarative way to handle errors differently depending on the source error type. -## Ad Hoc Error Raiser +## Ad Hoc Error Raisers One way that we can handle source errors differently is by defining an error raiser provider that has explicit implementation for each source error, such as follows: diff --git a/content/error-reporting.md b/content/error-reporting.md new file mode 100644 index 0000000..be73aa8 --- /dev/null +++ b/content/error-reporting.md @@ -0,0 +1,618 @@ +# Error Reporting + +In the [previous chapter on error handling](./error-handling.md), we implemented `AuthTokenValidator` +to raise the error string `"auth token has expired"`, when a given auth token has expired. +Even after we defined a custom error type `ErrAuthTokenHasExpired`, it is still a dummy +struct that has a `Debug` implementation that outputs the same string +`"auth token has expired"`. +In real world applications, we know that it is good engineering practice to include +as much details to an error, so that developers and end users can more easily +diagnose the source of the problem. +On the other hand, it takes a lot of effort to properly design and show good error +messages. When doing initial development, we don't necessary want to spend too +much effort on formatting error messages, when we don't even know if the code +would survive the initial iteration. + +To resolve the dilemma, developers are often forced to choose a comprehensive +error library that can do everything from error handling to error reporting. +Once the library is chosen, implementation code often becomes tightly coupled with +the error library. If there is any detail missing in the error report, it may +be challenging to include more details without diving deep into the impementation. + +CGP offers better ways to resolve this dilemma, by allowing us to decouple the +logic of error handling from actual error reporting. In this chapter, we will +go into detail of how we can use CGP to improve the error report to show +more information about an expired auth token. + +## Reporting Errors with Abstract Types + +One challenge that CGP introduces is that with abstract types, it may be challenging +to produce good error report without knowledge about the underlying type. +We can workaround this in a naive way by using impl-side dependencies to require +the abstract types `Context::AuthToken` and `Context::Time` to implement `Debug`, +and then format them as a string before raising it as an error: + +```rust +# extern crate cgp; +# +# use core::fmt::Debug; +# +# use cgp::prelude::*; +# +# #[cgp_component { +# name: TimeTypeComponent, +# provider: ProvideTimeType, +# }] +# pub trait HasTimeType { +# type Time; +# } +# +# #[cgp_component { +# name: AuthTokenTypeComponent, +# provider: ProvideAuthTokenType, +# }] +# pub trait HasAuthTokenType { +# type AuthToken; +# } +# +# #[cgp_component { +# provider: AuthTokenValidator, +# }] +# pub trait CanValidateAuthToken: HasAuthTokenType + HasErrorType { +# fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Self::Error>; +# } +# +# #[cgp_component { +# provider: AuthTokenExpiryFetcher, +# }] +# pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType + HasErrorType { +# fn fetch_auth_token_expiry( +# &self, +# auth_token: &Self::AuthToken, +# ) -> Result; +# } +# +# #[cgp_component { +# provider: CurrentTimeGetter, +# }] +# pub trait HasCurrentTime: HasTimeType + HasErrorType { +# fn current_time(&self) -> Result; +# } +# +pub struct ValidateTokenIsNotExpired; + +impl AuthTokenValidator for ValidateTokenIsNotExpired +where + Context: HasCurrentTime + CanFetchAuthTokenExpiry + for<'a> CanRaiseError, + Context::Time: Debug + Ord, + Context::AuthToken: Debug, +{ + fn validate_auth_token( + context: &Context, + auth_token: &Context::AuthToken, + ) -> Result<(), Context::Error> { + let now = context.current_time()?; + + let token_expiry = context.fetch_auth_token_expiry(auth_token)?; + + if token_expiry < now { + Ok(()) + } else { + Err(Context::raise_error( + format!( + "the auth token {:?} has expired at {:?}, which is earlier than the current time {:?}", + auth_token, token_expiry, now, + ))) + } + } +} +``` + +The example above now shows better error message. But our provider `ValidateTokenIsNotExpired` is now +tightly coupled with how the token expiry error is reported. We are now forced to implement `Debug` +for any `AuthToken` and `Time` types that we want to use. It is also not possible to customize the +error report to instead use the `Display` instance, without directly modifying the implementation +for `ValidateTokenIsNotExpired`. Similarly, we cannot easily customize how the message content is +formatted, or add additional details to the report. + +## Source Error Types with Abstract Fields + +To better report the error message, we would first re-introduce the `ErrAuthTokenHasExpired` source +error type that we have used in earlier examples. But now, we would also add fields with +_abstract types_ into the struct, so that it contains all values that may be essential for +generating a good error report: + +```rust +# extern crate cgp; +# +# use core::fmt::Debug; +# +# use cgp::prelude::*; +# +# #[cgp_component { +# name: TimeTypeComponent, +# provider: ProvideTimeType, +# }] +# pub trait HasTimeType { +# type Time; +# } +# +# #[cgp_component { +# name: AuthTokenTypeComponent, +# provider: ProvideAuthTokenType, +# }] +# pub trait HasAuthTokenType { +# type AuthToken; +# } +# +pub struct ErrAuthTokenHasExpired<'a, Context> +where + Context: HasAuthTokenType + HasTimeType, +{ + pub context: &'a Context, + pub auth_token: &'a Context::AuthToken, + pub current_time: &'a Context::Time, + pub expiry_time: &'a Context::Time, +} +``` + +The `ErrAuthTokenHasExpired` struct is now parameterized by a generic lifetime `'a` +and a generic context `Context`. Inside the struct, all fields are in the form of +reference `&'a`, so that we don't perform any copy to construct the error value. +The struct has a `where` clause to require `Context` to implement `HasAuthTokenType` +and `HasTimeType`, since we need to hold their values inside the struct. +In addition to `auth_token`, `current_time`, and `expiry_time`, we also include +a `context` field with a reference to the main context, so that additional error details +may be provided through `Context`. + +In addition to the struct, we also manually implement a `Debug` instance as a +default way to format `ErrAuthTokenHasExpired` as string: + +```rust +# extern crate cgp; +# +# use core::fmt::Debug; +# +# use cgp::prelude::*; +# +# #[cgp_component { +# name: TimeTypeComponent, +# provider: ProvideTimeType, +# }] +# pub trait HasTimeType { +# type Time; +# } +# +# #[cgp_component { +# name: AuthTokenTypeComponent, +# provider: ProvideAuthTokenType, +# }] +# pub trait HasAuthTokenType { +# type AuthToken; +# } +# +# pub struct ErrAuthTokenHasExpired<'a, Context> +# where +# Context: HasAuthTokenType + HasTimeType, +# { +# pub context: &'a Context, +# pub auth_token: &'a Context::AuthToken, +# pub current_time: &'a Context::Time, +# pub expiry_time: &'a Context::Time, +# } +# +impl<'a, Context> Debug for ErrAuthTokenHasExpired<'a, Context> +where + Context: HasAuthTokenType + HasTimeType, + Context::AuthToken: Debug, + Context::Time: Debug, +{ + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!( + f, + "the auth token {:?} has expired at {:?}, which is earlier than the current time {:?}", + self.auth_token, self.expiry_time, self.current_time, + ) + } +} +``` + +Inside the `Debug` instance for `ErrAuthTokenHasExpired`, we make use of impl-side dependencies +to require `Context::AuthToken` and `Context::Time` to implement `Debug`. We then use `Debug` +to format the values and show the error message. + +Notice that even though `ErrAuthTokenHasExpired` contains a `context` field, it is not used +in the `Debug` implementation. Also, since the `Debug` constraint for `Context::AuthToken` and +`Context::Time` are only present in the `Debug` implementation, it is possible for the concrete +types to not implement `Debug`, _if_ the application do not use `Debug` with `ErrAuthTokenHasExpired`. + +This design is intentional, as we only provide the `Debug` implementation as a _convenience_ +for quickly formatting the error message without further customization. +On the other hand, a better error reporting strategy may be present elsewhere and provided +by the application. +The main purpose of this design is so that at the time `ErrAuthTokenHasExpired` and +`ValidateTokenIsNotExpired` are defined, we don't need to concern about where and how +this error reporting strategy is implemented. + +Using the new `ErrAuthTokenHasExpired`, we can now re-implement `ValidateTokenIsNotExpired` +as follows: + +```rust +# extern crate cgp; +# +# use core::fmt::Debug; +# +# use cgp::prelude::*; +# +# #[cgp_component { +# name: TimeTypeComponent, +# provider: ProvideTimeType, +# }] +# pub trait HasTimeType { +# type Time; +# } +# +# #[cgp_component { +# name: AuthTokenTypeComponent, +# provider: ProvideAuthTokenType, +# }] +# pub trait HasAuthTokenType { +# type AuthToken; +# } +# +# #[cgp_component { +# provider: AuthTokenValidator, +# }] +# pub trait CanValidateAuthToken: HasAuthTokenType + HasErrorType { +# fn validate_auth_token(&self, auth_token: &Self::AuthToken) -> Result<(), Self::Error>; +# } +# +# #[cgp_component { +# provider: AuthTokenExpiryFetcher, +# }] +# pub trait CanFetchAuthTokenExpiry: HasAuthTokenType + HasTimeType + HasErrorType { +# fn fetch_auth_token_expiry( +# &self, +# auth_token: &Self::AuthToken, +# ) -> Result; +# } +# +# #[cgp_component { +# provider: CurrentTimeGetter, +# }] +# pub trait HasCurrentTime: HasTimeType + HasErrorType { +# fn current_time(&self) -> Result; +# } +# +# pub struct ErrAuthTokenHasExpired<'a, Context> +# where +# Context: HasAuthTokenType + HasTimeType, +# { +# pub context: &'a Context, +# pub auth_token: &'a Context::AuthToken, +# pub current_time: &'a Context::Time, +# pub expiry_time: &'a Context::Time, +# } +# +# impl<'a, Context> Debug for ErrAuthTokenHasExpired<'a, Context> +# where +# Context: HasAuthTokenType + HasTimeType, +# Context::AuthToken: Debug, +# Context::Time: Debug, +# { +# fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { +# write!( +# f, +# "the auth token {:?} has expired at {:?}, which is earlier than the current time {:?}", +# self.auth_token, self.expiry_time, self.current_time, +# ) +# } +# } +# +pub struct ValidateTokenIsNotExpired; + +impl AuthTokenValidator for ValidateTokenIsNotExpired +where + Context: HasCurrentTime + + CanFetchAuthTokenExpiry + + for<'a> CanRaiseError>, + Context::Time: Ord, +{ + fn validate_auth_token( + context: &Context, + auth_token: &Context::AuthToken, + ) -> Result<(), Context::Error> { + let now = context.current_time()?; + + let token_expiry = context.fetch_auth_token_expiry(auth_token)?; + + if token_expiry < now { + Ok(()) + } else { + Err(Context::raise_error(ErrAuthTokenHasExpired { + context, + auth_token, + current_time: &now, + expiry_time: &token_expiry, + })) + } + } +} +``` + +In the new implementation, we include the constraint +`for<'a> CanRaiseError>` +with [_higher ranked trait bound_](https://doc.rust-lang.org/nomicon/hrtb.html), +so that we can raise `ErrAuthTokenHasExpired` parameterized with any lifetime. +Notice that inside the `where` constraints, we no longer require the `Debug` +bound on `Context::AuthToken` and `Context::Time`. + +With this approach, we have made use of `ErrAuthTokenHasExpired` to fully +decouple `ValidateTokenIsNotExpired` provider from the problem of how to report +the token expiry error. + +## Error Report Raisers + +In the [previous chapter](./delegated-error-raiser.md), we have learned about +how to define custom error raisers and then dispatch them using the `UseDelegate` +pattern. With that in mind, we can easily define error raisers for +`ErrAuthTokenHasExpired` to format it in different ways. + +One thing to note is that since `ErrAuthTokenHasExpired` contains a lifetime +parameter with borrowed values, any error raiser that handles it would +likely have to make use of the borrowed value to construct an owned value +for `Context::Error`. + +The simplest way to raise `ErrAuthTokenHasExpired` is to make use of its `Debug` +implementation to and raise it using `DebugError`: + +```rust +# extern crate cgp; +# +use cgp::core::error::{CanRaiseError, ErrorRaiser}; +use core::fmt::Debug; + +pub struct DebugError; + +impl ErrorRaiser for DebugError +where + Context: CanRaiseError, + SourceError: Debug, +{ + fn raise_error(e: SourceError) -> Context::Error { + Context::raise_error(format!("{e:?}")) + } +} +``` + +As we discussed in the previous chapter, `DebugError` would implement `ErrorRaiser` +if `ErrAuthTokenHasExpired` implements `Debug`. But recall that the `Debug` implementation +for `ErrAuthTokenHasExpired` requires both `Context::AuthToken` and `Context::Time` to +implement `Debug`. So in a way, the use of impl-side dependencies here is _deeply nested_, +but nevertheless still works thanks to Rust's trait system. + +Now supposed that instead of using `Debug`, we want to use the `Display` instance of +`Context::AuthToken` and `Context::Time` to format the error. Even if we are in a crate +that do not own `ErrAuthTokenHasExpired`, we can still implement a custom `ErrorRaiser` +instance as follows: + +```rust +# extern crate cgp; +# +# use core::fmt::Display; +# +# use cgp::prelude::*; +# use cgp::core::error::ErrorRaiser; +# +# #[cgp_component { +# name: TimeTypeComponent, +# provider: ProvideTimeType, +# }] +# pub trait HasTimeType { +# type Time; +# } +# +# #[cgp_component { +# name: AuthTokenTypeComponent, +# provider: ProvideAuthTokenType, +# }] +# pub trait HasAuthTokenType { +# type AuthToken; +# } +# +# pub struct ErrAuthTokenHasExpired<'a, Context> +# where +# Context: HasAuthTokenType + HasTimeType, +# { +# pub context: &'a Context, +# pub auth_token: &'a Context::AuthToken, +# pub current_time: &'a Context::Time, +# pub expiry_time: &'a Context::Time, +# } +# +pub struct DisplayAuthTokenExpiredError; + +impl<'a, Context> ErrorRaiser> + for DisplayAuthTokenExpiredError +where + Context: HasAuthTokenType + HasTimeType + CanRaiseError, + Context::AuthToken: Display, + Context::Time: Display, +{ + fn raise_error(e: ErrAuthTokenHasExpired<'a, Context>) -> Context::Error { + Context::raise_error(format!( + "the auth token {} has expired at {}, which is earlier than the current time {}", + e.auth_token, e.expiry_time, e.current_time, + )) + } +} +``` + +With this approach, we can now use `DisplayAuthTokenExpiredError` if `Context::AuthToken` +and `Context::Time` implement `Display`. But even if they don't, we are still free to choose +alternative strategies for our application. + +One possible way to improve the error message is to obfuscate the auth token, so that the +reader of the error message cannot know about the actual auth token. This may have already +been done, if the concrete `AuthToken` type implements a custom `Display` that does so. +But in case if it does not, we can still do something similar using a customized error raiser: + + +```rust +# extern crate cgp; +# extern crate sha1; +# +# use core::fmt::Display; +# +# use cgp::prelude::*; +# use cgp::core::error::ErrorRaiser; +use sha1::{Digest, Sha1}; + +# #[cgp_component { +# name: TimeTypeComponent, +# provider: ProvideTimeType, +# }] +# pub trait HasTimeType { +# type Time; +# } +# +# #[cgp_component { +# name: AuthTokenTypeComponent, +# provider: ProvideAuthTokenType, +# }] +# pub trait HasAuthTokenType { +# type AuthToken; +# } +# +# pub struct ErrAuthTokenHasExpired<'a, Context> +# where +# Context: HasAuthTokenType + HasTimeType, +# { +# pub context: &'a Context, +# pub auth_token: &'a Context::AuthToken, +# pub current_time: &'a Context::Time, +# pub expiry_time: &'a Context::Time, +# } +# +pub struct ShowAuthTokenExpiredError; + +impl<'a, Context> ErrorRaiser> + for ShowAuthTokenExpiredError +where + Context: HasAuthTokenType + HasTimeType + CanRaiseError, + Context::AuthToken: Display, + Context::Time: Display, +{ + fn raise_error(e: ErrAuthTokenHasExpired<'a, Context>) -> Context::Error { + let auth_token_hash = Sha1::new_with_prefix(e.auth_token.to_string()).finalize(); + + Context::raise_error(format!( + "the auth token {:x} has expired at {}, which is earlier than the current time {}", + auth_token_hash, e.expiry_time, e.current_time, + )) + } +} +``` + +By decoupling the error reporting from the provider, we can now customize the error reporting +as we see fit, without needing to access or modify the original provider `ValidateTokenIsNotExpired`. + +## Context-Specific Error Details + +Previously, we included the `context` field in `ErrAuthTokenHasExpired` but never used it in +the error reporting. But with the ability to define custom error raisers, we can also +define one that extracts additional details from the context, so that it can be included +in the error message. + +Supposed that we are using `CanValidateAuthToken` in an application that serves sensitive documents. +When an expired auth token is used, we may want to also include the document ID being accessed, +so that we can identify the attack patterns of any potential attacker. +If the application context holds the document ID, we can now access it within the error raiser +as follows: + + +```rust +# extern crate cgp; +# extern crate sha1; +# +# use core::fmt::Display; +# +# use cgp::prelude::*; +# use cgp::core::error::ErrorRaiser; +use sha1::{Digest, Sha1}; + +# #[cgp_component { +# name: TimeTypeComponent, +# provider: ProvideTimeType, +# }] +# pub trait HasTimeType { +# type Time; +# } +# +# #[cgp_component { +# name: AuthTokenTypeComponent, +# provider: ProvideAuthTokenType, +# }] +# pub trait HasAuthTokenType { +# type AuthToken; +# } +# +# pub struct ErrAuthTokenHasExpired<'a, Context> +# where +# Context: HasAuthTokenType + HasTimeType, +# { +# pub context: &'a Context, +# pub auth_token: &'a Context::AuthToken, +# pub current_time: &'a Context::Time, +# pub expiry_time: &'a Context::Time, +# } +# +#[cgp_component { + provider: DocumentIdGetter, +}] +pub trait HasDocumentId { + fn document_id(&self) -> u64; +} + +pub struct ShowAuthTokenExpiredError; + +impl<'a, Context> ErrorRaiser> + for ShowAuthTokenExpiredError +where + Context: HasAuthTokenType + HasTimeType + CanRaiseError + HasDocumentId, + Context::AuthToken: Display, + Context::Time: Display, +{ + fn raise_error(e: ErrAuthTokenHasExpired<'a, Context>) -> Context::Error { + let document_id = e.context.document_id(); + let auth_token_hash = Sha1::new_with_prefix(e.auth_token.to_string()).finalize(); + + Context::raise_error(format!( + "failed to access highly sensitive document {} at time {}, using the auth token {:x} which was expired at {}", + document_id, e.current_time, auth_token_hash, e.expiry_time, + )) + } +} +``` + +With this, even though the provider `ValidateTokenIsNotExpired` did not know that `Context` contains +a document ID, by including the `context` value in `ErrAuthTokenHasExpired`, we can +still implement a custom error raiser that produce a custom error message that includes the document ID. + +## Conclusion + +In this chapter, we have learned about some advanced CGP techniques that can be used to decouple providers +from the burden of producing good error reports. With that, we are able to define custom error raisers +that produce highly detailed error reports, without needing to modify the original provider implementation. +The use of source error types with abstract fields and borrowed values serves as a cheap interface to decouple +the producer of an error (the provider) from the handler of an error (the error raiser). + +Still, even with CGP, learning all the best practices of properly raising and handling errors can be overwhelming, +especially for beginners. Furthermore, even if we can decouple and customize the handling of all possible error +cases, extra effort is still needed for every customization, which can still takes a lot of time. + +As a result, we do not encourage readers to try and define custom error structs for all +possible errors. Instead, readers should start with simple error types like strings, and slowly add more structures +to common errors that occur in the application. +But readers should keep in mind the techniques introduced in this chapter, so that by the time we need to +customize and produce good error reports for our applications, we know about how this can be done using CGP.