├── .cargo └── config.toml ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── rust-toolchain.toml └── src ├── actix.rs ├── axum.rs ├── commons.rs ├── custom.rs ├── ext.rs ├── http.rs ├── lib.rs ├── macros.rs ├── reporter.rs └── sql.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [target.'cfg(all())'] 2 | rustflags = [ 3 | # Allows 4 | "-Aclippy::let_unit_value", 5 | "-Aclippy::extra_unused_lifetimes", # <- Problems with diesel::Insertable macro expansion. 6 | "-Aclippy::use_self", # <- Problems with macro expansions. 7 | "-Aclippy::result_large_err", 8 | "-Aclippy::diverging_sub_expression", # <- Problems with async traits 9 | "-Aclippy::map_unwrap_or", # <- Separate unwrap_or sometimes is more clear 10 | 11 | "-Aclippy::doc_lazy_continuation", 12 | "-Aclippy::too_long_first_doc_paragraph", 13 | 14 | # Denies 15 | "-Drustdoc::broken_intra_doc_links", 16 | "-Dclippy::disallowed_methods", 17 | 18 | # Warns 19 | "-Wmissing_docs", 20 | "-Wrust_2018_idioms", 21 | "-Wunsafe_code", 22 | "-Wunused_imports", 23 | "-Wvariant_size_differences", 24 | "-Wlet_underscore_drop", 25 | "-Wclippy::checked_conversions", 26 | "-Wclippy::cloned_instead_of_copied", 27 | # "-Wclippy::cognitive_complexity", # <- Too much shit to fix, enable by service first 28 | "-Wclippy::dbg_macro", 29 | "-Wclippy::decimal_literal_representation", 30 | "-Wclippy::empty_enum", 31 | "-Wclippy::enum_glob_use", 32 | "-Wclippy::equatable_if_let", 33 | "-Wclippy::explicit_deref_methods", 34 | "-Wclippy::explicit_into_iter_loop", 35 | "-Wclippy::explicit_iter_loop", 36 | "-Wclippy::filter_map_next", 37 | "-Wclippy::filter_map_next", 38 | "-Wclippy::fn_params_excessive_bools", 39 | "-Wclippy::if_not_else", 40 | "-Wclippy::implicit_clone", 41 | "-Wclippy::implicit_saturating_sub", 42 | "-Wclippy::imprecise_flops", 43 | "-Wclippy::index_refutable_slice", 44 | "-Wclippy::items_after_statements", 45 | "-Wclippy::large_types_passed_by_value", 46 | "-Wclippy::manual_filter_map", 47 | "-Wclippy::manual_find_map", 48 | "-Wclippy::manual_ok_or", 49 | "-Wclippy::manual_unwrap_or", 50 | "-Wclippy::map_flatten", 51 | "-Wclippy::match_same_arms", 52 | #"-Wclippy::missing_const_for_fn", <- too many false positives. 53 | "-Wclippy::missing_errors_doc", 54 | "-Wclippy::mut_mut", 55 | "-Wclippy::needless_continue", 56 | "-Wclippy::needless_pass_by_value", 57 | "-Wclippy::ref_option_ref", 58 | "-Wclippy::single_match_else", 59 | "-Wclippy::manual_let_else", 60 | "-Wclippy::suspicious_operation_groupings", 61 | "-Wclippy::type_repetition_in_bounds", 62 | "-Wclippy::unnecessary_sort_by", 63 | "-Wclippy::unnecessary_wraps", 64 | "-Wclippy::useless_format", 65 | ] # Enable global lints 66 | 67 | [registries.crates-io] 68 | protocol = "sparse" 69 | 70 | [build] 71 | rustflags = ["-Z", "threads=8"] 72 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "http-problem" 3 | version = "0.4.0" 4 | authors = ["Luis Holanda "] 5 | edition = "2021" 6 | license = "MIT OR Apache-2.0" 7 | description = "A HTTP APIs focused error handling library" 8 | homepage = "https://github.com/terramagna/http-problem" 9 | repository = "https://github.com/terramagna/http-problem" 10 | keywords = ["http", "api", "error", "7807"] 11 | categories = ["web-programming"] 12 | include = ["src/**/*.rs"] 13 | 14 | [features] 15 | diesel = ["dep:diesel", "sql"] 16 | actix = ["dep:actix-web"] 17 | axum = ["dep:axum"] 18 | sql = [] 19 | sqlx = ["dep:sqlx-core", "sql"] 20 | tokio-postgres = ["dep:tokio-postgres", "sql"] 21 | 22 | [dependencies] 23 | actix-web = { version = "4.4.0", default-features = false, optional = true } 24 | axum = { version = "0.7", optional = true } 25 | backtrace = "0.3.69" 26 | chrono = "0.4.19" 27 | eyre = "0.6.9" 28 | http = "1" 29 | parking_lot = "0.12.1" 30 | serde = { version = "1.0.193", features = ["derive"] } 31 | serde_json = "1.0.108" 32 | sqlx-core = { version = "0.7.3", default-features = false, optional = true } 33 | 34 | [dependencies.diesel] 35 | version = "1.4.8" 36 | default-features = false 37 | features = ["r2d2"] 38 | optional = true 39 | 40 | [dependencies.tokio-postgres] 41 | version = "0.7.10" 42 | default-features = false 43 | optional = true 44 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 TerraMagna 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 TerraMagna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.60.0" 3 | components = ["rustfmt", "clippy"] 4 | -------------------------------------------------------------------------------- /src/actix.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | http::{header, StatusCode}, 3 | Either, HttpResponse, ResponseError, 4 | }; 5 | 6 | use crate::{http, CowStr, Problem, Result}; 7 | 8 | impl ResponseError for Problem { 9 | fn status_code(&self) -> StatusCode { 10 | StatusCode::from_u16(self.status().as_u16()).expect("status is valid") 11 | } 12 | 13 | fn error_response(&self) -> HttpResponse { 14 | self.report_as_error(); 15 | 16 | HttpResponse::build(self.status_code()) 17 | .insert_header((header::CONTENT_TYPE, "application/problem+json")) 18 | .json(self) 19 | } 20 | } 21 | 22 | /// Extension trait for actix-web's `Either` type, which provide additional 23 | /// methods for a more ergonomic error handling. 24 | pub trait EitherExt { 25 | /// Returns a result with the value of the `Left` variant. 26 | /// 27 | /// # Errors 28 | /// 29 | /// Returns a `BadRequest` (with the provided message) if the container 30 | /// doesn't hold a `Left`. 31 | fn try_left(self, msg: impl Into) -> Result; 32 | /// Returns a result with the value of the `Right` variant. 33 | /// 34 | /// # Errors 35 | /// 36 | /// Returns a `BadRequest` (with the provided message) if the container 37 | /// doesn't hold a `Right`. 38 | fn try_right(self, msg: impl Into) -> Result; 39 | } 40 | 41 | impl EitherExt for Either { 42 | #[track_caller] 43 | fn try_left(self, msg: impl Into) -> Result { 44 | match self { 45 | Either::Left(val) => Ok(val), 46 | Either::Right(_) => Err(http::bad_request(msg)), 47 | } 48 | } 49 | 50 | #[track_caller] 51 | fn try_right(self, msg: impl Into) -> Result { 52 | match self { 53 | Either::Left(_) => Err(http::bad_request(msg)), 54 | Either::Right(val) => Ok(val), 55 | } 56 | } 57 | } 58 | 59 | #[cfg(test)] 60 | mod tests { 61 | use actix_web::body::MessageBody; 62 | 63 | use super::*; 64 | 65 | #[test] 66 | fn test_response_error() { 67 | let err = crate::http::failed_precondition(); 68 | 69 | let actix_status = ::http::StatusCode::from_u16(err.status_code().as_u16()).unwrap(); 70 | assert_eq!(actix_status, err.status()); 71 | 72 | let resp = err.error_response(); 73 | let actix_status = ::http::StatusCode::from_u16(resp.status().as_u16()).unwrap(); 74 | assert_eq!(actix_status, err.status()); 75 | assert_eq!( 76 | resp.headers().get(header::CONTENT_TYPE).unwrap().as_bytes(), 77 | b"application/problem+json" 78 | ); 79 | 80 | let body = resp.into_body().try_into_bytes().unwrap(); 81 | 82 | assert_eq!(body, serde_json::to_vec(&err).unwrap()); 83 | } 84 | 85 | #[test] 86 | fn try_left_works() { 87 | let a = Either::::Left(1); 88 | assert_eq!(1, a.try_left("a err").unwrap()); 89 | 90 | let b = Either::::Right(2); 91 | let err = b.try_left("b err").unwrap_err(); 92 | assert!(err.is::()); 93 | assert_eq!("b err", err.details()); 94 | } 95 | 96 | #[test] 97 | fn try_right_works() { 98 | let a = Either::::Left(1); 99 | let err = a.try_right("a err").unwrap_err(); 100 | assert!(err.is::()); 101 | assert_eq!("a err", err.details()); 102 | 103 | let b = Either::::Right(2); 104 | assert_eq!(2, b.try_right("b err").unwrap()); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/axum.rs: -------------------------------------------------------------------------------- 1 | use axum::{ 2 | http::header, 3 | response::{IntoResponse, Json, Response}, 4 | }; 5 | 6 | use crate::Problem; 7 | 8 | impl IntoResponse for Problem { 9 | fn into_response(self) -> Response { 10 | self.report_as_error(); 11 | 12 | ( 13 | self.status(), 14 | [(header::CONTENT_TYPE, "application/problem+json")], 15 | Json(&self), 16 | ) 17 | .into_response() 18 | } 19 | } 20 | 21 | #[cfg(test)] 22 | mod tests { 23 | use super::*; 24 | 25 | #[test] 26 | fn test_into_response() { 27 | let err = crate::http::failed_precondition(); 28 | 29 | let resp = crate::http::failed_precondition().into_response(); 30 | 31 | assert_eq!(resp.status(), err.status()); 32 | assert_eq!( 33 | resp.headers().get(header::CONTENT_TYPE).unwrap().as_bytes(), 34 | b"application/problem+json" 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/commons.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::{custom::StatusCode, CowStr}; 4 | 5 | /// An error emitted when an assertion failed to be satisfied. 6 | /// 7 | /// Emitted by [`ensure!`]. 8 | /// 9 | /// [`ensure!`]: crate::ensure 10 | #[derive(Debug, Clone)] 11 | pub struct AssertionError { 12 | msg: CowStr, 13 | } 14 | 15 | impl fmt::Display for AssertionError { 16 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 | write!(f, "assertion failure: {}", self.msg) 18 | } 19 | } 20 | 21 | impl std::error::Error for AssertionError {} 22 | 23 | impl AssertionError { 24 | /// Message stored in this error. 25 | #[inline(always)] 26 | pub fn message(&self) -> &str { 27 | &self.msg 28 | } 29 | 30 | #[doc(hidden)] 31 | pub const fn new_static(msg: &'static str) -> Self { 32 | Self { 33 | msg: CowStr::Borrowed(msg), 34 | } 35 | } 36 | 37 | #[doc(hidden)] 38 | pub const fn new(msg: String) -> Self { 39 | Self { 40 | msg: CowStr::Owned(msg), 41 | } 42 | } 43 | } 44 | 45 | impl From for crate::Problem { 46 | #[track_caller] 47 | fn from(err: AssertionError) -> Self { 48 | Self::from_status(StatusCode::INTERNAL_SERVER_ERROR) 49 | .with_detail("An unexpected error occurred") 50 | .with_cause(err) 51 | } 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use super::*; 57 | use crate::Problem; 58 | 59 | #[test] 60 | fn test_assert_static() { 61 | let err = AssertionError::new_static("bla"); 62 | assert_eq!(err.message(), "bla"); 63 | 64 | let prb = Problem::from(err.clone()); 65 | 66 | assert_eq!(prb.status(), StatusCode::INTERNAL_SERVER_ERROR); 67 | assert_ne!(prb.details(), err.message()); 68 | } 69 | 70 | #[test] 71 | fn test_assert_owned() { 72 | let err = AssertionError::new("bla".to_string()); 73 | assert_eq!(err.message(), "bla"); 74 | 75 | let prb = Problem::from(err.clone()); 76 | 77 | assert_eq!(prb.status(), StatusCode::INTERNAL_SERVER_ERROR); 78 | assert_ne!(prb.details(), err.message()); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/custom.rs: -------------------------------------------------------------------------------- 1 | pub use http::{StatusCode, Uri}; 2 | 3 | use super::{CowStr, Extensions, Problem}; 4 | 5 | /// Define a new custom problem type. 6 | /// 7 | /// Although defining a new custom type requires only implementing 8 | /// the [`CustomProblem`] trait, this macro simplifies the code, 9 | /// removing boilerplate from the definition. 10 | /// 11 | /// # Example 12 | /// 13 | /// ``` 14 | /// use http_problem::prelude::{StatusCode, Uri}; 15 | /// http_problem::define_custom_type! { 16 | /// /// An error that occurs when a transaction cannot be done 17 | /// /// because one of the accounts doesn't have enough credit. 18 | /// type OutOfCredit { 19 | /// type: "https://example.com/probs/out-of-credit", 20 | /// title: "You do not have enough credit", 21 | /// status: StatusCode::FORBIDDEN, 22 | /// detail(p): format!("You current balance is {}, but that costs {}", p.balance, p.cost), 23 | /// extensions: { 24 | /// balance: i64, 25 | /// cost: i64, 26 | /// accounts: Vec 27 | /// } 28 | /// } 29 | /// } 30 | /// 31 | /// fn do_transaction() -> http_problem::Result<()> { 32 | /// Err(OutOfCredit { 33 | /// balance: 50, 34 | /// cost: 30, 35 | /// accounts: vec!["/account/12345".into(), "/account/67890".into()] 36 | /// }.into()) 37 | /// } 38 | /// 39 | /// fn main() { 40 | /// let problem = do_transaction().unwrap_err(); 41 | /// assert_eq!(problem.type_(), &Uri::from_static("https://example.com/probs/out-of-credit")); 42 | /// assert_eq!(problem.title(), "You do not have enough credit"); 43 | /// assert_eq!(problem.status(), StatusCode::FORBIDDEN); 44 | /// assert_eq!(problem.details(), "You current balance is 50, but that costs 30"); 45 | /// assert_eq!(problem.extensions().len(), 3); 46 | /// } 47 | /// ``` 48 | #[macro_export] 49 | macro_rules! define_custom_type { 50 | ($(#[$meta: meta])* type $rstyp: ident { 51 | type: $typ:literal, 52 | title: $title:literal, 53 | status: $status: expr, 54 | detail($prob: ident): $detail: expr, 55 | extensions: { 56 | $($field:ident: $field_ty: ty),* $(,)? 57 | } $(,)? 58 | }) => { 59 | $(#[$meta])* 60 | #[derive(Debug)] 61 | pub struct $rstyp { 62 | $(pub $field: $field_ty),* 63 | } 64 | 65 | impl ::std::fmt::Display for $rstyp { 66 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 67 | writeln!(f, "{}", ::details(self)) 68 | } 69 | } 70 | 71 | impl ::std::error::Error for $rstyp {} 72 | 73 | impl $crate::prelude::CustomProblem for $rstyp { 74 | fn problem_type(&self) -> $crate::prelude::Uri { 75 | $crate::prelude::Uri::from_static($typ) 76 | } 77 | 78 | fn title(&self) -> &'static str { 79 | $title 80 | } 81 | 82 | fn status_code(&self) -> $crate::prelude::StatusCode { 83 | $status 84 | } 85 | 86 | fn details(&self) -> $crate::CowStr { 87 | let $prob = self; 88 | $detail.into() 89 | } 90 | 91 | fn add_extensions( 92 | &self, 93 | _extensions: &mut $crate::Extensions) 94 | { 95 | $( 96 | _extensions.insert(stringify!($field), &self.$field); 97 | )* 98 | } 99 | } 100 | }; 101 | } 102 | 103 | /// A trait defining custom problem types. 104 | /// 105 | /// Implementing this trait provides enough information to create a 106 | /// [`Problem`] instance with the correct values for each field. 107 | /// 108 | /// There is no need to implement `From for Problem` if you 109 | /// implement this trait. 110 | /// 111 | /// See [`define_custom_type!`] for a convenient way of implementing 112 | /// this trait. 113 | pub trait CustomProblem: std::error::Error + Send + Sync + 'static { 114 | /// A URI reference that identifies the problem type. 115 | /// 116 | /// See [`Problem::type_`] more information. 117 | fn problem_type(&self) -> Uri; 118 | 119 | /// A short, human-readable summary of the problem type. 120 | /// 121 | /// See [`Problem::title`] for more information. 122 | fn title(&self) -> &'static str; 123 | 124 | /// The HTTP status code for this problem type. 125 | /// 126 | /// See [`Problem::status`] for more information. 127 | fn status_code(&self) -> StatusCode; 128 | 129 | /// A human-readable explanation of the occurrence. 130 | /// 131 | /// See [`Problem::details`] for more information. 132 | fn details(&self) -> CowStr; 133 | 134 | /// Add extensions to the final problem instance. 135 | /// 136 | /// See [`Problem::with_extension`] for more info. 137 | fn add_extensions(&self, extensions: &mut Extensions); 138 | } 139 | 140 | impl From for Problem { 141 | #[track_caller] 142 | fn from(custom: C) -> Self { 143 | let mut problem = Self::custom(custom.status_code(), custom.problem_type()) 144 | .with_title(custom.title()) 145 | .with_detail(custom.details()); 146 | 147 | custom.add_extensions(problem.extensions_mut()); 148 | 149 | problem.with_cause(custom) 150 | } 151 | } 152 | 153 | #[cfg(test)] 154 | mod tests { 155 | use super::*; 156 | 157 | define_custom_type! { 158 | /// An error that occurs when a transaction cannot be done 159 | /// because one of the accounts doesn't have enough credit. 160 | type OutOfCredit { 161 | type: "https://example.com/probs/out-of-credit", 162 | title: "You do not have enough credit", 163 | status: StatusCode::FORBIDDEN, 164 | detail(p): format!("You current balance is {}, but that costs {}", p.balance, p.cost), 165 | extensions: { 166 | balance: i64, 167 | cost: i64, 168 | accounts: Vec 169 | } 170 | } 171 | } 172 | 173 | #[test] 174 | fn test_macro_output() { 175 | let error = OutOfCredit { 176 | balance: 30, 177 | cost: 50, 178 | accounts: vec!["aaa".into(), "bbb".into()], 179 | }; 180 | 181 | assert_eq!(error.title(), "You do not have enough credit"); 182 | assert_eq!(error.status_code(), StatusCode::FORBIDDEN); 183 | assert_eq!( 184 | error.details(), 185 | "You current balance is 30, but that costs 50" 186 | ); 187 | } 188 | 189 | #[test] 190 | fn test_custom_problem_to_problem() { 191 | let error = OutOfCredit { 192 | balance: 30, 193 | cost: 50, 194 | accounts: vec!["aaa".into(), "bbb".into()], 195 | }; 196 | 197 | let prob: Problem = error.into(); 198 | 199 | assert_eq!(prob.title(), "You do not have enough credit"); 200 | assert_eq!(prob.status(), StatusCode::FORBIDDEN); 201 | assert_eq!( 202 | prob.details(), 203 | "You current balance is 30, but that costs 50" 204 | ); 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/ext.rs: -------------------------------------------------------------------------------- 1 | //! # Standard Error Handling Types Extensions 2 | //! 3 | //! This module provides traits that extends the common error 4 | //! handling types `Result` and `Option` with methods that 5 | //! integrate them with the types defined in this crate. 6 | use std::{borrow::Cow, error::Error}; 7 | 8 | #[cfg(feature = "diesel")] 9 | use crate::sql::NoRowsFound; 10 | use crate::{ 11 | http::{self, NotFound}, 12 | Result, 13 | }; 14 | 15 | mod sealed { 16 | pub trait Sealed { 17 | type Value; 18 | } 19 | 20 | impl Sealed for Result { 21 | type Value = T; 22 | } 23 | 24 | impl Sealed for Option { 25 | type Value = T; 26 | } 27 | } 28 | 29 | /// Extension methods on [`Result`]. 30 | /// 31 | /// [`Result`]: std::result::Result 32 | pub trait ResultExt: sealed::Sealed + Sized { 33 | /// Converts this result to an internal error. 34 | /// 35 | /// Used when an unrecoverable error happens. 36 | /// 37 | /// See [`internal_error`] for more information. 38 | /// 39 | /// # Errors 40 | /// 41 | /// Returns `Err` if `self` is `Err`. 42 | /// 43 | /// [`internal_error`]: crate::http::internal_error 44 | fn internal(self) -> Result; 45 | } 46 | 47 | impl ResultExt for Result 48 | where 49 | E: Error + Send + Sync + 'static, 50 | { 51 | #[track_caller] 52 | fn internal(self) -> Result { 53 | // NOTE: Can't use map_err as it doesn't has track_caller. 54 | match self { 55 | Ok(t) => Ok(t), 56 | Err(err) => Err(http::internal_error(err)), 57 | } 58 | } 59 | } 60 | 61 | /// Extension methods on `Result`. 62 | pub trait ProblemResultExt: ResultExt { 63 | /// Catch a specific error type `E`. 64 | /// 65 | /// Returns `Ok(Ok(_))` when the source `Result` is a `T`, returns 66 | /// `Ok(Err(_))` when its is a `Problem` that can downcast to an `E`, 67 | /// and returns `Err(_)` otherwise. 68 | /// 69 | /// Useful when there is a need to handle a specific error differently, 70 | /// e.g. a [`NotFound`] error. 71 | /// 72 | /// # Errors 73 | /// 74 | /// Returns `Err` if `self` contains a [`Problem`] which is not an `E`. 75 | /// 76 | /// [`Problem`]: crate::Problem 77 | /// [`NotFound`]: crate::http::NotFound 78 | fn catch_err(self) -> Result> 79 | where 80 | E: Error + Send + Sync + 'static; 81 | 82 | /// Catch a [`NotFound`] and convert it to `None`. 83 | /// 84 | /// # Errors 85 | /// 86 | /// Returns `Err` if `self` contains a [`Problem`] which is not a 87 | /// [`NotFound`]. 88 | /// 89 | /// [`Problem`]: crate::Problem 90 | /// [`NotFound`]: crate::http::NotFound 91 | fn optional(self) -> Result>; 92 | } 93 | 94 | impl ProblemResultExt for Result { 95 | fn catch_err(self) -> Result> 96 | where 97 | E: Error + Send + Sync + 'static, 98 | { 99 | Ok(match self { 100 | Ok(ok) => Ok(ok), 101 | Err(err) => Err(err.downcast::()?), 102 | }) 103 | } 104 | 105 | fn optional(self) -> Result> { 106 | match self { 107 | Ok(ok) => Ok(Some(ok)), 108 | Err(err) => { 109 | #[allow(clippy::question_mark)] 110 | if let Err(err) = err.downcast::() { 111 | #[cfg(feature = "diesel")] 112 | err.downcast::()?; 113 | #[cfg(not(feature = "diesel"))] 114 | return Err(err); 115 | } 116 | 117 | Ok(None) 118 | } 119 | } 120 | } 121 | } 122 | 123 | /// Extension methods on `Option`. 124 | pub trait OptionExt: sealed::Sealed + Sized { 125 | /// Returns `Ok(_)` if the source is `Some(_)`, otherwise, returns a 126 | /// `Problem` that can downcast to `NotFound`. 127 | /// 128 | /// # Errors 129 | /// 130 | /// Returns `Err` when `self` is `None`. 131 | fn or_not_found(self, entity: &'static str, identifier: I) -> Result 132 | where 133 | I: std::fmt::Display; 134 | 135 | /// Returns `Ok(_)` if the source is `Some(_)`, otherwise, returns a 136 | /// `Problem` that can downcast to `Unprocessable`. This method is 137 | /// useful when failing to find the entity prevents the request from being 138 | /// processed. 139 | /// 140 | /// # Errors 141 | /// 142 | /// Returns `Err` when `self` is `None`. 143 | fn or_unprocessable(self, message: &'static str) -> Result; 144 | 145 | /// It's a wrapper to `or_not_found` to be used when 146 | /// there is no a specific identifier to entity message. 147 | /// 148 | /// Returns `Ok(_)` if the source is `Some(_)`, otherwise, returns a 149 | /// `Problem` that can downcast to `NotFound`. 150 | /// 151 | /// # Errors 152 | /// 153 | /// Returns `Err` when `self` is `None`. 154 | fn or_not_found_unknown(self, entity: &'static str) -> Result; 155 | 156 | /// Returns `Ok(_)` if the source is `Some(_)`, otherwise, returns a 157 | /// `Problem` that can downcast to `Unprocessable`. This method is 158 | /// useful when failing to find the entity prevents the request from being 159 | /// processed. Differently from `or_unprocessable`, this method receives a 160 | /// closure to avoid unnecessary allocations. 161 | /// 162 | /// # Errors 163 | /// 164 | /// Returns `Err` when `self` is `None` 165 | fn or_unprocessable_with(self, message_fn: F) -> Result 166 | where 167 | F: FnOnce() -> M, 168 | M: Into>; 169 | } 170 | 171 | impl OptionExt for Option { 172 | #[track_caller] 173 | fn or_not_found(self, entity: &'static str, identifier: I) -> Result 174 | where 175 | I: std::fmt::Display, 176 | { 177 | // Cannot use Option::ok_or_else as it isn't annotated with track_caller. 178 | if let Some(value) = self { 179 | Ok(value) 180 | } else { 181 | Err(http::not_found(entity, identifier)) 182 | } 183 | } 184 | 185 | #[track_caller] 186 | fn or_unprocessable(self, message: &'static str) -> Result { 187 | // Cannot use Option::ok_or_else as it isn't annotated with track_caller. 188 | if let Some(value) = self { 189 | Ok(value) 190 | } else { 191 | Err(http::unprocessable(message)) 192 | } 193 | } 194 | 195 | #[track_caller] 196 | fn or_unprocessable_with(self, message_fn: F) -> Result 197 | where 198 | F: FnOnce() -> M, 199 | M: Into>, 200 | { 201 | // Cannot use Option::ok_or_else as it isn't annotated with track_caller. 202 | if let Some(value) = self { 203 | Ok(value) 204 | } else { 205 | Err(http::unprocessable(message_fn())) 206 | } 207 | } 208 | 209 | #[track_caller] 210 | fn or_not_found_unknown(self, entity: &'static str) -> Result { 211 | self.or_not_found(entity, "") 212 | } 213 | } 214 | 215 | #[cfg(test)] 216 | mod tests { 217 | use super::*; 218 | use crate::http; 219 | 220 | #[test] 221 | fn test_internal() { 222 | let res = 223 | Err(std::io::Error::new(std::io::ErrorKind::Other, "oh no")) as std::io::Result<()>; 224 | 225 | let res = res.internal().unwrap_err(); 226 | 227 | assert!(res.is::()); 228 | } 229 | 230 | #[test] 231 | fn test_catch_err() { 232 | let res = 233 | Err(std::io::Error::new(std::io::ErrorKind::Other, "oh no")) as std::io::Result<()>; 234 | 235 | let res = res.internal(); 236 | 237 | let not_found = res.catch_err::().unwrap_err(); 238 | let res = Err(not_found) as crate::Result<()>; 239 | 240 | let res = res.catch_err::().unwrap(); 241 | 242 | assert!(res.is_err()); 243 | 244 | let ok = Ok(()) as crate::Result<()>; 245 | 246 | assert!(ok.catch_err::().unwrap().is_ok()); 247 | } 248 | 249 | #[test] 250 | fn test_optional() { 251 | let res = Err(http::not_found("user", "bla")) as crate::Result<()>; 252 | assert!(res.optional().unwrap().is_none()); 253 | 254 | let res = Err(http::failed_precondition()) as crate::Result<()>; 255 | assert!(res.optional().is_err()); 256 | 257 | let res = Ok(()) as crate::Result<()>; 258 | assert!(res.optional().unwrap().is_some()); 259 | } 260 | 261 | #[test] 262 | fn test_or_not_found() { 263 | let res = None.or_not_found_unknown("bla") as crate::Result<()>; 264 | let err = res.unwrap_err(); 265 | 266 | assert!(err.is::()); 267 | 268 | let res = Some(()).or_not_found_unknown("bla"); 269 | 270 | assert!(res.is_ok()); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/http.rs: -------------------------------------------------------------------------------- 1 | use std::{error::Error, fmt}; 2 | 3 | pub use crate::custom::{StatusCode, Uri}; 4 | use crate::{custom::CustomProblem, CowStr, Extensions, Problem}; 5 | 6 | /// Create a [`Problem`] instance with a [`BadRequest`] cause. 7 | #[track_caller] 8 | pub fn bad_request(msg: impl Into) -> Problem { 9 | Problem::from(BadRequest { msg: msg.into() }) 10 | } 11 | 12 | /// Create a [`Problem`] instance with an [`Unauthorized`] cause. 13 | #[track_caller] 14 | pub fn unauthorized() -> Problem { 15 | Problem::from(Unauthorized { _inner: () }) 16 | } 17 | 18 | /// Create a [`Problem`] instance with an [`Forbidden`] cause. 19 | #[track_caller] 20 | pub fn forbidden() -> Problem { 21 | Problem::from(Forbidden { _inner: () }) 22 | } 23 | 24 | /// Create a [`Problem`] instance with a [`NotFound`] cause. 25 | #[track_caller] 26 | pub fn not_found(entity: &'static str, identifier: impl fmt::Display) -> Problem { 27 | Problem::from(NotFound { 28 | entity, 29 | identifier: identifier.to_string(), 30 | }) 31 | } 32 | 33 | /// Create a [`Problem`] instance with a [`NotFound`] cause with an unknown 34 | /// identifier. 35 | #[track_caller] 36 | pub fn not_found_unknown(entity: &'static str) -> Problem { 37 | not_found(entity, "") 38 | } 39 | 40 | /// Create a [`Problem`] instance with a [`Conflict`] cause. 41 | #[track_caller] 42 | pub fn conflict(msg: impl Into) -> Problem { 43 | Problem::from(Conflict { msg: msg.into() }) 44 | } 45 | 46 | /// Create a [`Problem`] instance with a [`PreconditionFailed`] cause. 47 | #[track_caller] 48 | pub fn failed_precondition() -> Problem { 49 | Problem::from(PreconditionFailed { _inner: () }) 50 | } 51 | 52 | /// Create a [`Problem`] instance with an [`UnprocessableEntity`] cause. 53 | #[track_caller] 54 | pub fn unprocessable(msg: impl Into) -> Problem { 55 | Problem::from(UnprocessableEntity { msg: msg.into() }) 56 | } 57 | 58 | /// Create a [`Problem`] instance with no available cause. 59 | /// 60 | /// This error is meant to be used when an _unrecoverable_ error happens. Here, 61 | /// unrecoverable means errors that upper levels doesn't have any means to 62 | /// recover from other than retrying the operation or propagating it up. 63 | #[track_caller] 64 | pub fn internal_error(msg: M) -> Problem 65 | where 66 | M: Error + Send + Sync + 'static, 67 | { 68 | Problem::from_status(StatusCode::INTERNAL_SERVER_ERROR) 69 | .with_detail("An unexpected error occurred") 70 | .with_cause(InternalError { 71 | inner: Box::new(msg), 72 | }) 73 | } 74 | 75 | /// Create a [`Problem`] instance with a [`ServiceUnavailable`] cause. 76 | #[track_caller] 77 | pub fn service_unavailable() -> Problem { 78 | Problem::from(ServiceUnavailable { _inner: () }) 79 | } 80 | 81 | macro_rules! http_problem_type { 82 | ($status: ident) => { 83 | fn problem_type(&self) -> Uri { 84 | crate::blank_type_uri() 85 | } 86 | 87 | fn title(&self) -> &'static str { 88 | self.status_code().canonical_reason().unwrap() 89 | } 90 | 91 | fn status_code(&self) -> StatusCode { 92 | StatusCode::$status 93 | } 94 | }; 95 | 96 | ($status: ident, display) => { 97 | http_problem_type!($status); 98 | 99 | fn details(&self) -> CowStr { 100 | self.to_string().into() 101 | } 102 | }; 103 | 104 | ($status: ident, details: $detail: expr) => { 105 | http_problem_type!($status); 106 | 107 | fn details(&self) -> CowStr { 108 | $detail.into() 109 | } 110 | }; 111 | 112 | ($status: ident, details <- $detail: ident) => { 113 | http_problem_type!($status); 114 | 115 | fn details(&self) -> CowStr { 116 | self.$detail.to_string().into() 117 | } 118 | }; 119 | } 120 | 121 | macro_rules! zst_problem_type { 122 | ($(#[$doc:meta])+ $name:ident, $status_code:ident, $details:literal) => { 123 | $(#[$doc])+ 124 | #[derive(Debug, Copy, Clone)] 125 | pub struct $name { 126 | _inner: (), 127 | } 128 | 129 | impl fmt::Display for $name { 130 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 131 | f.write_str(stringify!($name)) 132 | } 133 | } 134 | 135 | impl Error for $name {} 136 | 137 | impl CustomProblem for $name { 138 | http_problem_type!( 139 | $status_code, 140 | details: $details 141 | ); 142 | 143 | fn add_extensions(&self, _: &mut Extensions) {} 144 | } 145 | } 146 | } 147 | 148 | /// A `400 - Bad Request` error. 149 | /// 150 | /// Used when the request is syntactically wrong. 151 | #[derive(Debug)] 152 | pub struct BadRequest { 153 | msg: CowStr, 154 | } 155 | 156 | impl fmt::Display for BadRequest { 157 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 158 | f.write_str(&self.msg) 159 | } 160 | } 161 | 162 | impl Error for BadRequest {} 163 | 164 | impl CustomProblem for BadRequest { 165 | http_problem_type!(BAD_REQUEST, details <- msg); 166 | 167 | fn add_extensions(&self, _: &mut Extensions) {} 168 | } 169 | 170 | zst_problem_type!( 171 | /// A `401 - Unauthorized` HTTP error. 172 | /// 173 | /// Used when a session doesn't have access to a given service. 174 | Unauthorized, 175 | UNAUTHORIZED, 176 | "You don't have the necessary permissions" 177 | ); 178 | 179 | zst_problem_type!( 180 | /// A `403 - Forbidden` HTTP error. 181 | /// 182 | /// Used when a session doesn't have access to a given resource. 183 | Forbidden, 184 | FORBIDDEN, 185 | "You don't have the necessary permissions" 186 | ); 187 | 188 | zst_problem_type!( 189 | /// A `419 - Precondition Failed` error. 190 | /// 191 | /// Used when some precondition in a condition request could not be 192 | /// satisfied. 193 | PreconditionFailed, 194 | PRECONDITION_FAILED, 195 | "Some request precondition could not be satisfied." 196 | ); 197 | 198 | /// A `404 - Not Found` error. 199 | /// 200 | /// Used when the application expected some entity to exist, but it didn't. 201 | #[derive(Debug)] 202 | pub struct NotFound { 203 | identifier: String, 204 | entity: &'static str, 205 | } 206 | 207 | impl NotFound { 208 | /// The type of entity not found. 209 | pub const fn entity(&self) -> &'static str { 210 | self.entity 211 | } 212 | } 213 | 214 | impl fmt::Display for NotFound { 215 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 216 | write!( 217 | f, 218 | "The {} identified by '{}' wasn't found", 219 | self.entity, self.identifier 220 | ) 221 | } 222 | } 223 | 224 | impl Error for NotFound {} 225 | 226 | impl CustomProblem for NotFound { 227 | http_problem_type!(NOT_FOUND, display); 228 | 229 | fn add_extensions(&self, extensions: &mut Extensions) { 230 | extensions.insert("identifier", &self.identifier); 231 | extensions.insert("entity", self.entity); 232 | } 233 | } 234 | 235 | /// A `409 - Conflict` error. 236 | /// 237 | /// Used when a code invariant was broken due to a client provided information. 238 | #[derive(Debug, Clone)] 239 | pub struct Conflict { 240 | msg: CowStr, 241 | } 242 | 243 | impl fmt::Display for Conflict { 244 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 245 | write!(f, "Conflict: {}", self.msg) 246 | } 247 | } 248 | 249 | impl Error for Conflict {} 250 | 251 | impl CustomProblem for Conflict { 252 | http_problem_type!(CONFLICT, details <- msg); 253 | 254 | fn add_extensions(&self, _: &mut Extensions) {} 255 | } 256 | 257 | /// A `422 - Unprocessable Entity` error. 258 | /// 259 | /// Used when the received client information doesn't follow the expected 260 | /// interface and requirements. 261 | #[derive(Debug)] 262 | pub struct UnprocessableEntity { 263 | msg: CowStr, 264 | } 265 | 266 | impl fmt::Display for UnprocessableEntity { 267 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 268 | write!(f, "Unprocessable Entity: {}", self.msg) 269 | } 270 | } 271 | 272 | impl Error for UnprocessableEntity {} 273 | 274 | impl CustomProblem for UnprocessableEntity { 275 | http_problem_type!(UNPROCESSABLE_ENTITY, details <- msg); 276 | 277 | fn add_extensions(&self, _: &mut Extensions) {} 278 | } 279 | 280 | /// A `500 - Internal Server Error` error. 281 | /// 282 | /// Used when there is an unexpected situation or when an unrecoverable error 283 | /// occurs. 284 | #[derive(Debug)] 285 | pub struct InternalError { 286 | inner: Box, 287 | } 288 | 289 | impl fmt::Display for InternalError { 290 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 291 | self.inner.fmt(f) 292 | } 293 | } 294 | 295 | impl Error for InternalError { 296 | fn source(&self) -> Option<&(dyn Error + 'static)> { 297 | Some(&*self.inner) 298 | } 299 | } 300 | 301 | zst_problem_type!( 302 | /// A `503 - Service Unavailable` error. 303 | /// 304 | /// Used when a necessary resource for the correctly execution of the 305 | /// operation was unavailable (e.g. a database connection). 306 | ServiceUnavailable, 307 | SERVICE_UNAVAILABLE, 308 | "The service is currently unavailable, try again after an exponential backoff" 309 | ); 310 | 311 | #[cfg(test)] 312 | mod tests { 313 | use super::*; 314 | 315 | macro_rules! test_constructor { 316 | ($test_fn: ident, $constructor: ident, $ty: ty $(,$arg: expr)*) => { 317 | #[test] 318 | fn $test_fn() { 319 | let prd = $constructor($($arg),*); 320 | 321 | assert!(prd.is::<$ty>()); 322 | } 323 | }; 324 | } 325 | 326 | test_constructor!(test_bad_request, bad_request, BadRequest, "bla"); 327 | test_constructor!(test_unauthorized, unauthorized, Unauthorized); 328 | test_constructor!(test_forbidden, forbidden, Forbidden); 329 | test_constructor!(test_not_found, not_found, NotFound, "bla", "foo"); 330 | test_constructor!(test_conflict, conflict, Conflict, "bla"); 331 | test_constructor!( 332 | test_failed_precondition, 333 | failed_precondition, 334 | PreconditionFailed 335 | ); 336 | test_constructor!( 337 | test_unprocessable, 338 | unprocessable, 339 | UnprocessableEntity, 340 | "bla" 341 | ); 342 | test_constructor!( 343 | test_internal_error, 344 | internal_error, 345 | InternalError, 346 | std::io::Error::new(std::io::ErrorKind::Other, "bla") 347 | ); 348 | test_constructor!( 349 | test_service_unavailable, 350 | service_unavailable, 351 | ServiceUnavailable 352 | ); 353 | } 354 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! # HTTP Problem-based Error Handling Library 2 | //! 3 | //! This crate provides a general mechanism for error handling based on the 4 | //! [RFC 7807] problem entity with the [`Problem`] type. 5 | //! 6 | //! Users can find many pre-defined errors at the [`http`] and [`sql`] modules. 7 | //! 8 | //! The workflow for error handling with this library is as follow: 9 | //! 10 | //! 1. Use the predefined errors/functions or define a new one with the 11 | //! [`define_custom_type!`] macro to returns errors in functions that return 12 | //! [`Result`] (an alias is provided in the library). 13 | //! * You can also use the extensions traits [`ResultExt`], 14 | //! [`ProblemResultExt`], [`OptionExt`] to handle common cases. 15 | //! 2. Catch any desired error with [`ProblemResultExt::catch_err`]. 16 | //! 17 | //! [RFC 7807]: https://tools.ietf.org/html/rfc7807 18 | //! [`Problem`]: crate::Problem 19 | //! [`define_custom_type!`]: crate::define_custom_type 20 | //! [`http`]: crate::http 21 | //! [`sql`]: crate::sql 22 | //! [`Result`]: crate::Result 23 | //! [`ResultExt`]: crate::ext::ResultExt 24 | //! [`ProblemResultExt`]: crate::ext::ProblemResultExt 25 | //! [`OptionExt`]: crate::ext::OptionExt 26 | //! [`ProblemResultExt::catch_err`]: crate::ext::ProblemResultExt::catch_err 27 | use std::{borrow::Cow, collections::HashMap, panic::Location}; 28 | 29 | use backtrace::Backtrace; 30 | use eyre::EyreContext; 31 | use parking_lot::Once; 32 | use serde::ser::SerializeMap; 33 | 34 | mod macros; 35 | 36 | #[cfg(feature = "actix")] 37 | mod actix; 38 | #[cfg(feature = "axum")] 39 | mod axum; 40 | mod commons; 41 | mod custom; 42 | pub(crate) use self::custom::*; 43 | mod ext; 44 | /// HTTP related well-known errors. 45 | pub mod http; 46 | /// Definitions for global error reporting. 47 | pub mod reporter; 48 | use self::reporter::Report; 49 | #[cfg(feature = "sql")] 50 | /// SQL related well-known errors. 51 | pub mod sql; 52 | 53 | /// Prelude imports for this crate. 54 | pub mod prelude { 55 | #[cfg(feature = "actix")] 56 | pub use super::actix::*; 57 | #[cfg(feature = "sql")] 58 | pub use super::sql::*; 59 | pub use super::{commons::*, custom::*, ext::*, http, Problem, Result}; 60 | } 61 | 62 | pub(crate) fn blank_type_uri() -> custom::Uri { 63 | custom::Uri::from_static("about:blank") 64 | } 65 | 66 | /// An alias for a static `Cow`. 67 | pub type CowStr = Cow<'static, str>; 68 | 69 | /// Convenience alias for functions that can error ouy with [`Problem`]. 70 | pub type Result = std::result::Result; 71 | 72 | fn install() { 73 | static HOOK_INSTALLED: Once = Once::new(); 74 | 75 | HOOK_INSTALLED.call_once(|| { 76 | eyre::set_hook(Box::new(crate::reporter::capture_handler)) 77 | .expect("Failed to set error hook, maybe install was already called?"); 78 | }) 79 | } 80 | 81 | /// A [RFC 7807] Problem Error. 82 | /// 83 | /// # Error Cause 84 | /// 85 | /// This type provides methods to access the inner error cause. Although we 86 | /// store it, we DO NOT send it when serializing the problem, as it would 87 | /// leak implementation details. 88 | /// 89 | /// # Backtraces 90 | /// 91 | /// Many implementations of the RFC add automatic backtrace to the problem. 92 | /// This is NOT done by this type and MUST NOT be added manually, as exposing 93 | /// the backtrace to the caller will expose implementation details and CAN 94 | /// be source of vulnerabilities. 95 | /// 96 | /// # Custom Problem Types 97 | /// 98 | /// When an HTTP API needs to define a response that indicates an error 99 | /// condition, it might be appropriate to do so by defining a new problem type. 100 | /// 101 | /// New problem type definitions MUST document: 102 | /// 103 | /// 1. a type URI (typically, with the "http" or "https" scheme), 104 | /// 2. a title that appropriately describes it (think short), and 105 | /// 3. the HTTP status code for it to be used with. 106 | /// 107 | /// A problem type definition MAY specify additional members on the problem 108 | /// details object. For example, an extension might use typed links [RFC 5988] 109 | /// to another resource that can be used by machines to resolve the problem. 110 | /// 111 | /// Avoid defining custom problem types, preferring to use standardized HTTP 112 | /// status whenever possible. Custom types should only be defined if no 113 | /// HTTP status code can properly encode the occurred problem. As an example: 114 | /// 115 | /// ```ignore 116 | /// { 117 | /// "type": "https://example.com/probs/out-of-credit", 118 | /// "status": 403, 119 | /// "title": "You do not have enough credit", 120 | /// "detail": "Your current balance is 30, but that costs 50", 121 | /// "balance": 30, 122 | /// "accounts": ["/account/12345", "/account/67890"] 123 | /// } 124 | /// ``` 125 | /// 126 | /// When adding a new problem type, we suggest that the type reference should 127 | /// also be added to the main API gateway page. 128 | /// 129 | /// # Error Instances 130 | /// 131 | /// We currently do not track error instances (the `instance` field defined 132 | /// in the RFC). This may change in the future. 133 | /// 134 | /// [RFC 7807]: https://tools.ietf.org/html/rfc7807 135 | /// [RFC 5988]: https://tools.ietf.org/html/rfc5988 136 | #[derive(Default)] 137 | pub struct Problem { 138 | inner: Box, 139 | } 140 | 141 | #[derive(Debug)] 142 | struct ProblemInner { 143 | r#type: Uri, 144 | title: CowStr, 145 | status: StatusCode, 146 | details: CowStr, 147 | cause: eyre::Report, 148 | extensions: Extensions, 149 | } 150 | 151 | impl Default for ProblemInner { 152 | fn default() -> Self { 153 | Self { 154 | r#type: blank_type_uri(), 155 | title: Cow::Borrowed(""), 156 | status: StatusCode::default(), 157 | details: Cow::Borrowed(""), 158 | cause: eyre::Report::msg(""), 159 | extensions: Extensions::default(), 160 | } 161 | } 162 | } 163 | 164 | impl ProblemInner { 165 | fn report(&self) -> &Report { 166 | self.cause 167 | .handler() 168 | .downcast_ref::() 169 | .expect("Problem used without installation") 170 | } 171 | } 172 | 173 | impl serde::Serialize for Problem { 174 | fn serialize(&self, serializer: S) -> Result 175 | where 176 | S: serde::Serializer, 177 | { 178 | let mut map = serializer.serialize_map(None)?; 179 | 180 | map.serialize_entry(&"status", &self.status().as_u16())?; 181 | 182 | if !matches!(self.type_().scheme_str(), None | Some("about")) { 183 | map.serialize_entry(&"type", &format_args!("{}", self.type_()))?; 184 | } 185 | 186 | map.serialize_entry(&"title", &self.title())?; 187 | map.serialize_entry(&"detail", &self.details())?; 188 | 189 | for (k, v) in &self.extensions().inner { 190 | map.serialize_entry(k, v)?; 191 | } 192 | 193 | map.end() 194 | } 195 | } 196 | 197 | impl std::fmt::Debug for Problem { 198 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 199 | self.inner.report().debug(self.cause(), f) 200 | } 201 | } 202 | 203 | impl std::fmt::Display for Problem { 204 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 205 | use eyre::EyreHandler; 206 | 207 | writeln!( 208 | f, 209 | "{} - {}: {}", 210 | self.status(), 211 | self.title(), 212 | self.details() 213 | )?; 214 | self.inner.report().display(&*self.inner.cause, f)?; 215 | 216 | Ok(()) 217 | } 218 | } 219 | 220 | impl std::error::Error for Problem { 221 | fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { 222 | Some(self.cause()) 223 | } 224 | } 225 | 226 | impl Problem { 227 | /// Reports this problem to the global reporter. 228 | pub fn report_as_error(&self) { 229 | if let Some(reporter) = self::reporter::global_reporter() { 230 | if reporter.should_report_error(self) { 231 | reporter.report_error(self); 232 | } 233 | } 234 | } 235 | } 236 | 237 | /// [`Problem`] constructors. 238 | impl Problem { 239 | /// Create a custom problem from the given type. 240 | /// 241 | /// See the type's documentation for more information about custom types. 242 | #[track_caller] 243 | pub fn custom(status: StatusCode, r#type: Uri) -> Self { 244 | let mut problem = Self::from_status(status); 245 | problem.inner.r#type = r#type; 246 | problem 247 | } 248 | 249 | /// Create a new problem for the given status code. 250 | #[track_caller] 251 | pub fn from_status(status: StatusCode) -> Self { 252 | install(); 253 | 254 | let title = status.canonical_reason().unwrap(); 255 | Self { 256 | inner: Box::new(ProblemInner { 257 | title: title.into(), 258 | cause: eyre::Report::msg(title), 259 | status, 260 | ..ProblemInner::default() 261 | }), 262 | } 263 | } 264 | 265 | /// Sets the title for this problem. The title is visible to the user. 266 | /// 267 | /// **OBS**: HTTP Status only problems MUST NOT have their title changed. 268 | /// 269 | /// This method doesn't protect against this, be sure to follow the spec. 270 | #[must_use] 271 | pub fn with_title(mut self, title: impl Into) -> Self { 272 | self.inner.title = title.into(); 273 | self 274 | } 275 | 276 | /// Sets the detail for this problem. The title is visible to the user. 277 | #[must_use] 278 | pub fn with_detail(mut self, detail: impl Into) -> Self { 279 | self.inner.details = detail.into(); 280 | self 281 | } 282 | 283 | /// Sets the error cause for this problem. The cause is not visible to the 284 | /// user. 285 | #[must_use] 286 | #[track_caller] 287 | pub fn with_cause(mut self, cause: E) -> Self 288 | where 289 | E: std::error::Error + Send + Sync + 'static, 290 | { 291 | self.inner.cause = eyre::Report::new(cause); 292 | self 293 | } 294 | 295 | /// Add a new extension value for the problem. 296 | /// 297 | /// The `telemetry` extension is reserved for internal use and the `cause` 298 | /// extension is reserved for future use. 299 | /// 300 | /// # Panics 301 | /// 302 | /// Panics if `field == "cause"` or if the serialization of `value` fails. 303 | #[must_use] 304 | pub fn with_extension(mut self, extension: E, value: V) -> Self 305 | where 306 | E: Into, 307 | V: serde::Serialize, 308 | { 309 | let extension = extension.into(); 310 | match extension.as_ref() { 311 | "type" | "status" | "details" | "cause" | "" => { 312 | panic!("Invalid extension received: {extension}") 313 | } 314 | _ => self.inner.extensions.insert(extension, value), 315 | } 316 | 317 | self 318 | } 319 | } 320 | 321 | /// Getters 322 | impl Problem { 323 | /// A URI reference ([RFC 3986]) that identifies the problem type. 324 | /// 325 | /// The specification encourages that, when dereferenced, it provide 326 | /// human-readable documentation for the problem type. When this 327 | /// member is not present, its value is assumed to be `about:blank`. 328 | /// 329 | /// [RFC 3986]: https://tools.ietf.org/html/rfc3986 330 | pub const fn type_(&self) -> &Uri { 331 | &self.inner.r#type 332 | } 333 | 334 | /// A short, human-readable summary of the problem type. 335 | /// 336 | /// It SHOULD NOT change from occurrence to occurrence of the problem. 337 | pub fn title(&self) -> &str { 338 | &self.inner.title 339 | } 340 | 341 | /// The HTTP status code generated by the origin server for this 342 | /// occurrence of the problem. 343 | pub const fn status(&self) -> StatusCode { 344 | self.inner.status 345 | } 346 | 347 | /// A human-readable explanation specific to this occurrence of the 348 | /// problem. 349 | pub fn details(&self) -> &str { 350 | &self.inner.details 351 | } 352 | 353 | /// Extra members of the problem containing additional information 354 | /// about the specific occurrence. 355 | pub const fn extensions(&self) -> &Extensions { 356 | &self.inner.extensions 357 | } 358 | 359 | /// Extra members of the problem containing additional information 360 | /// about the specific occurrence. 361 | pub fn extensions_mut(&mut self) -> &mut Extensions { 362 | &mut self.inner.extensions 363 | } 364 | 365 | /// The internal cause of this problem. 366 | pub fn cause(&self) -> &(dyn std::error::Error + 'static) { 367 | &*self.inner.cause 368 | } 369 | } 370 | 371 | /// Error handling methods. 372 | impl Problem { 373 | /// Get the [`Report`] of this instance. 374 | #[must_use] 375 | pub fn report(&self) -> &Report { 376 | self.inner.report() 377 | } 378 | 379 | /// Get the backtrace for this Error. 380 | pub fn backtrace(&self) -> Backtrace { 381 | (*self.inner.report().backtrace()).clone() 382 | } 383 | 384 | /// Location where this instance was created. 385 | pub fn location(&self) -> &'static Location<'static> { 386 | self.inner.report().location() 387 | } 388 | 389 | /// Returns true if `E` is the type of the cause of this problem. 390 | /// 391 | /// Useful to a failed result is caused by a specific error type. 392 | pub fn is(&self) -> bool 393 | where 394 | E: std::error::Error + Send + Sync + 'static, 395 | { 396 | self.inner.cause.is::() 397 | } 398 | 399 | /// Attempts to downcast the problem to a concrete type. 400 | /// 401 | /// # Errors 402 | /// 403 | /// Returns the original problem if the underlying cause is not of the 404 | /// specified type. 405 | pub fn downcast(mut self) -> Result 406 | where 407 | E: std::error::Error + Send + Sync + 'static, 408 | { 409 | match self.inner.cause.downcast() { 410 | Ok(err) => Ok(err), 411 | Err(cause) => { 412 | self.inner.cause = cause; 413 | Err(self) 414 | } 415 | } 416 | } 417 | 418 | /// Attempt to downcast the problem to a concrete type by reference. 419 | pub fn downcast_ref(&self) -> Option<&E> 420 | where 421 | E: std::error::Error + Send + Sync + 'static, 422 | { 423 | self.inner.cause.downcast_ref() 424 | } 425 | 426 | /// Attempts to isolate a specific cause to the `Err` variant. 427 | /// 428 | /// This is different from a downcast as we don't lose backtrace/source 429 | /// location information. 430 | /// 431 | /// This method is useful when the user wants to handle specific errors 432 | /// with `?`. 433 | /// 434 | /// # Errors 435 | /// 436 | /// Returns `Err` when `self` is an `E`. 437 | pub fn isolate(self) -> Result 438 | where 439 | E: std::error::Error + Send + Sync + 'static, 440 | { 441 | if self.is::() { 442 | Err(self) 443 | } else { 444 | Ok(self) 445 | } 446 | } 447 | } 448 | 449 | /// Set of extensions of a [`Problem`]. 450 | #[derive(Debug, Clone, Default, serde::Serialize)] 451 | #[serde(transparent)] 452 | pub struct Extensions { 453 | inner: HashMap, 454 | } 455 | 456 | impl Extensions { 457 | /// Add an extension into the set. 458 | /// 459 | /// # Panics 460 | /// 461 | /// Panics if the serialization of `V` fails. 462 | pub fn insert(&mut self, key: K, value: V) 463 | where 464 | K: Into, 465 | V: serde::Serialize, 466 | { 467 | self.inner.insert(key.into(), serde_json::json!(value)); 468 | } 469 | 470 | /// Number of extensions. 471 | pub fn len(&self) -> usize { 472 | self.inner.len() 473 | } 474 | 475 | /// If we have no extensions. 476 | pub fn is_empty(&self) -> bool { 477 | self.inner.is_empty() 478 | } 479 | } 480 | 481 | impl<'e> IntoIterator for &'e Extensions { 482 | type IntoIter = ExtensionsIter<'e>; 483 | type Item = (&'e str, &'e serde_json::Value); 484 | 485 | fn into_iter(self) -> Self::IntoIter { 486 | ExtensionsIter(self.inner.iter().map(|(k, v)| (&**k, v))) 487 | } 488 | } 489 | 490 | use std::{collections::hash_map::Iter, iter::Map}; 491 | 492 | #[doc(hidden)] 493 | #[allow(clippy::type_complexity)] 494 | pub struct ExtensionsIter<'e>( 495 | Map< 496 | Iter<'e, Cow<'e, str>, serde_json::Value>, 497 | for<'a> fn((&'a Cow<'a, str>, &'a serde_json::Value)) -> (&'a str, &'a serde_json::Value), 498 | >, 499 | ); 500 | 501 | impl<'e> Iterator for ExtensionsIter<'e> { 502 | type Item = (&'e str, &'e serde_json::Value); 503 | 504 | fn next(&mut self) -> Option { 505 | self.0.next() 506 | } 507 | } 508 | 509 | #[cfg(test)] 510 | mod tests { 511 | use std::error::Error; 512 | 513 | use serde_json::json; 514 | 515 | use super::*; 516 | 517 | #[test] 518 | fn test_extensions() { 519 | let mut ext = Extensions::default(); 520 | 521 | assert!(ext.is_empty()); 522 | assert_eq!(ext.len(), 0); 523 | assert!(ext.into_iter().next().is_none()); 524 | 525 | ext.insert("bla", "bla"); 526 | 527 | assert_eq!(ext.len(), 1); 528 | assert!(!ext.is_empty()); 529 | assert_eq!(ext.into_iter().next(), Some(("bla", &json!("bla")))); 530 | 531 | assert_eq!(json!(ext), json!({ "bla": "bla" })); 532 | } 533 | 534 | #[test] 535 | fn test_problem_with_extensions_good() { 536 | let mut error = http::failed_precondition(); 537 | 538 | for (key, value) in [ 539 | ("bla", json!("bla")), 540 | ("foo", json!(1)), 541 | ("bar", json!(1.2)), 542 | ("baz", json!([1.2])), 543 | ] { 544 | error = error.with_extension(key, value); 545 | } 546 | 547 | assert_eq!(error.extensions().len(), 4); 548 | } 549 | 550 | macro_rules! test_invalid_extension { 551 | ($test_fn: ident, $ext: literal) => { 552 | #[test] 553 | #[should_panic = concat!("Invalid extension received: ", $ext)] 554 | fn $test_fn() { 555 | let _res = http::failed_precondition().with_extension($ext, json!(1)); 556 | } 557 | }; 558 | } 559 | 560 | test_invalid_extension!(test_problem_with_extension_type, "type"); 561 | test_invalid_extension!(test_problem_with_extension_status, "status"); 562 | test_invalid_extension!(test_problem_with_extension_details, "details"); 563 | test_invalid_extension!(test_problem_with_extension_cause, "cause"); 564 | test_invalid_extension!(test_problem_with_extension_empty, ""); 565 | 566 | #[test] 567 | fn test_problem_getter_type_() { 568 | assert_eq!(http::failed_precondition().type_(), "about:blank"); 569 | } 570 | 571 | #[test] 572 | fn test_problem_getter_report() { 573 | let err = http::failed_precondition(); 574 | let report = err.report(); 575 | 576 | assert_eq!(err.location(), report.location()); 577 | } 578 | 579 | #[test] 580 | fn test_problem_error_handling() { 581 | let err = http::failed_precondition(); 582 | 583 | assert!(err.is::()); 584 | assert!(err.downcast_ref::().is_some()); 585 | assert!(err.isolate::().is_err()); 586 | 587 | let err = http::failed_precondition(); 588 | assert!(!err.is::()); 589 | assert!(err.downcast_ref::().is_none()); 590 | assert!(err.isolate::().is_ok()); 591 | 592 | let err = http::failed_precondition(); 593 | assert!(err.downcast::().is_ok()); 594 | 595 | let err = http::failed_precondition(); 596 | assert!(err.downcast::().is_err()); 597 | } 598 | 599 | #[test] 600 | fn test_problem_source() { 601 | let err = http::failed_precondition(); 602 | let source = err.source().unwrap() as *const dyn Error as *const (); 603 | let cause = err.cause() as *const dyn Error as *const (); 604 | 605 | assert!(core::ptr::eq(source, cause)); 606 | } 607 | 608 | #[test] 609 | fn test_problem_serialize_no_type() { 610 | let err = http::failed_precondition() 611 | .with_detail("Failed a precondition") 612 | .with_extension("foo", "bar"); 613 | 614 | assert_eq!( 615 | json!(err), 616 | json!({ 617 | "detail": "Failed a precondition", 618 | "foo": "bar", 619 | "status": 412, 620 | "title": "Precondition Failed", 621 | }) 622 | ); 623 | } 624 | 625 | #[test] 626 | fn test_problem_serialize_type() { 627 | let err = Problem::custom( 628 | StatusCode::PRECONDITION_FAILED, 629 | Uri::from_static("https://my.beautiful.error"), 630 | ) 631 | .with_detail("Failed a precondition") 632 | .with_extension("foo", "bar"); 633 | 634 | assert_eq!( 635 | json!(err), 636 | json!({ 637 | "detail": "Failed a precondition", 638 | "foo": "bar", 639 | "status": 412, 640 | "title": "Precondition Failed", 641 | "type": "https://my.beautiful.error/", 642 | }) 643 | ); 644 | } 645 | } 646 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | /// Return early with an [`AssertionError`] if a condition is not satisfied. 2 | /// 3 | /// This macro is equivalent to [`assert!`], but returns a [`Problem`] instead 4 | /// of panicking. 5 | /// 6 | /// [`AssertionError`]: crate::prelude::AssertionError 7 | /// [`Problem`]: crate::Problem 8 | #[macro_export] 9 | macro_rules! ensure { 10 | ($check:expr, $msg:literal $(,)?) => { 11 | if !$check { 12 | return Err($crate::Problem::from($crate::prelude::AssertionError::new_static($msg))); 13 | } 14 | }; 15 | ($check:expr, $($arg:tt)+) => { 16 | if !$check { 17 | let msg = format!($($arg)+); 18 | return Err($crate::Problem::from($crate::prelude::AssertionError::new(msg))); 19 | } 20 | } 21 | } 22 | 23 | /// Return early with an [`UnprocessableEntity`] if a condition is not 24 | /// satisfied. 25 | /// 26 | /// [`UnprocessableEntity`]: crate::http::UnprocessableEntity 27 | #[macro_export] 28 | macro_rules! requires { 29 | ($check:expr, $msg:literal $(,)?) => { 30 | if !$check { 31 | return Err($crate::prelude::http::unprocessable($msg)); 32 | } 33 | }; 34 | ($check:expr, $($arg:tt)+) => { 35 | if !$check { 36 | return Err($crate::prelude::http::unprocessable(format!($($arg)+))); 37 | } 38 | } 39 | } 40 | 41 | #[cfg(test)] 42 | mod tests { 43 | #[test] 44 | fn test_ensure() { 45 | fn inner(cond: bool) -> crate::Result<()> { 46 | crate::ensure!(cond, "assertion"); 47 | 48 | Ok(()) 49 | } 50 | 51 | assert!(inner(true).is_ok()); 52 | 53 | let err = inner(false).unwrap_err(); 54 | assert!(err.is::()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/reporter.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | error::Error, 3 | fmt::{self, Debug}, 4 | ops::Deref, 5 | panic::Location, 6 | sync::OnceLock, 7 | }; 8 | 9 | use backtrace::Backtrace; 10 | use chrono::{DateTime, Local}; 11 | use http::Extensions; 12 | use parking_lot::RwLock; 13 | 14 | use crate::Problem; 15 | 16 | static REPORTER: OnceLock> = OnceLock::new(); 17 | 18 | #[track_caller] 19 | pub(crate) fn capture_handler(error: &(dyn Error + 'static)) -> Box { 20 | let mut report = Box::default(); 21 | 22 | if let Some(reporter) = global_reporter() { 23 | reporter.capture_error_context(&mut report, error); 24 | } 25 | 26 | report 27 | } 28 | 29 | pub(crate) fn global_reporter() -> Option<&'static dyn ProblemReporter> { 30 | REPORTER.get().map(|r| &**r as &'static dyn ProblemReporter) 31 | } 32 | 33 | /// Sets the current global reporter. 34 | /// 35 | /// # Panics 36 | /// 37 | /// This function panics if it is called twice. 38 | pub fn set_global_reporter(reporter: R) 39 | where 40 | R: ProblemReporter + Send + Sync + 'static, 41 | { 42 | // Can't use `.expect` as `R` is not (and should not) be `Debug`. 43 | if REPORTER.set(Box::new(reporter)).is_err() { 44 | panic!("Global problem reporter set twice! Did you call `problem::reporter::set_global_reporter` twice?"); 45 | } 46 | } 47 | 48 | /// A type responsible for capturing and report [`Problem`] errors. 49 | /// 50 | /// The crate doesn't provide a default implementation of this trait, 51 | /// as how you report your service's errors depends on your infrastructure. 52 | pub trait ProblemReporter { 53 | /// Capture extra information about the error context and saves it 54 | /// inside the given report. 55 | /// 56 | /// Note that this will be called even for client-related errors, 57 | /// like NotFound or UnprocessableEntity, during the error _creation_ 58 | /// (this is why we don't pass a [`Problem`] instance here). So try 59 | /// to be conservative on what you do in this method to prevent making 60 | /// error handling an expensive operation. 61 | fn capture_error_context(&'static self, report: &mut Report, error: &(dyn Error + 'static)); 62 | 63 | /// Says if we should report the error or not. 64 | fn should_report_error(&'static self, problem: &Problem) -> bool; 65 | 66 | /// Reports the error. 67 | /// 68 | /// This will only be called if [`ProblemReporter::should_report_error`] 69 | /// returns `true`. 70 | fn report_error(&'static self, problem: &Problem); 71 | } 72 | 73 | /// Location dependent problem report content. 74 | /// 75 | /// This type contains information about a given error, primarily the 76 | /// backtrace, location and timestamp of the error, although reporters may 77 | /// include extra information. 78 | /// 79 | /// Note that the [`Backtrace`] is NOT resolved during creation, to prevent 80 | /// wasting time on the creation of non-reported errors. 81 | pub struct Report { 82 | backtrace: RwLock, 83 | location: &'static Location<'static>, 84 | timestamp: DateTime, 85 | extensions: Extensions, 86 | } 87 | 88 | impl Default for Report { 89 | #[track_caller] 90 | fn default() -> Self { 91 | Self { 92 | backtrace: RwLock::new(Backtrace::new_unresolved()), 93 | location: Location::caller(), 94 | timestamp: Local::now(), 95 | extensions: Extensions::new(), 96 | } 97 | } 98 | } 99 | 100 | impl Report { 101 | /// Resolves and returns a reference to the error backtrace. 102 | pub fn backtrace(&self) -> impl Deref + '_ { 103 | self.backtrace.write().resolve(); 104 | self.backtrace.read() 105 | } 106 | 107 | /// Returns a reference to the _unresolved_ backtrace. 108 | #[inline(always)] 109 | pub fn backtrace_unresolved(&self) -> impl Deref + '_ { 110 | self.backtrace.read() 111 | } 112 | 113 | /// Returns the location where the error happened. 114 | /// 115 | /// We try our best to fetch the correct location of the error by 116 | /// marking everything that may create a [`Problem`] with `#[track_caller]`. 117 | #[inline(always)] 118 | pub fn location(&self) -> &'static Location<'static> { 119 | self.location 120 | } 121 | 122 | /// Returns the timestamp of when the error was captured. 123 | #[inline(always)] 124 | pub fn timestamp(&self) -> DateTime { 125 | self.timestamp 126 | } 127 | 128 | /// Inserts an arbitrary data into the report. 129 | #[inline(always)] 130 | pub fn insert(&mut self, val: T) { 131 | self.extensions.insert(val); 132 | } 133 | 134 | /// Get data inserted in the report via [`Self::insert`]. 135 | /// 136 | /// Returns `None` if no data with the given type is found. 137 | #[inline(always)] 138 | pub fn get(&self) -> Option<&T> { 139 | self.extensions.get() 140 | } 141 | } 142 | 143 | impl eyre::EyreHandler for Report { 144 | fn debug(&self, error: &(dyn Error + 'static), f: &mut fmt::Formatter<'_>) -> fmt::Result { 145 | write!( 146 | f, 147 | "Error at {} ({}, {}): ", 148 | self.location.file(), 149 | self.location.line(), 150 | self.location.column() 151 | )?; 152 | 153 | write!(f, "{error}")?; 154 | 155 | // Causes. 156 | if error.source().is_some() { 157 | writeln!(f, "\n\nCaused by:")?; 158 | 159 | let mut curr = error.source(); 160 | let mut idx = 0; 161 | while let Some(err) = curr { 162 | writeln!(f, " {idx}: {err}")?; 163 | curr = err.source(); 164 | idx += 1; 165 | } 166 | } 167 | 168 | // TODO: print backtrace based on backtrace style. 169 | // TODO(internal): Open-Source Backtrace cleaning solution. 170 | // 171 | // Initially, this used to print a cleaned-up version of the 172 | // backtrace, but the process had very TM-specific filters and was 173 | // specific to a format that our error reporting infrastructure understood. 174 | // 175 | // We'll one day open source it when we've a more general solution to 176 | // the problem, as this is definitively _extremely_ useful. 177 | (*self.backtrace()).fmt(f) 178 | } 179 | 180 | fn track_caller(&mut self, location: &'static Location<'static>) { 181 | self.location = location; 182 | } 183 | } 184 | 185 | #[cfg(test)] 186 | mod tests { 187 | use super::*; 188 | 189 | #[test] 190 | fn test_report() { 191 | let rep = Report::default(); 192 | 193 | assert_eq!(rep.location().line(), line!() - 2); 194 | assert_eq!(rep.location().file(), file!()); 195 | 196 | std::thread::sleep(std::time::Duration::from_millis(10)); 197 | 198 | assert!(rep.timestamp() < Local::now()); 199 | assert!(!rep.backtrace_unresolved().frames().is_empty()); 200 | 201 | let symbols_count = rep 202 | .backtrace() 203 | .frames() 204 | .iter() 205 | .flat_map(|f| f.symbols()) 206 | .count(); 207 | 208 | assert!(symbols_count > 0); 209 | } 210 | 211 | #[test] 212 | fn test_report_extensions() { 213 | let mut rep = Report::default(); 214 | 215 | rep.insert(2usize); 216 | 217 | assert_eq!(rep.get(), Some(&2usize)); 218 | 219 | assert!(rep.get::().is_none()); 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /src/sql.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "diesel")] 2 | use diesel::{ 3 | r2d2::PoolError, 4 | result::{DatabaseErrorInformation, DatabaseErrorKind, Error}, 5 | }; 6 | #[cfg(feature = "tokio-postgres")] 7 | use tokio_postgres::error::{Error as PgError, SqlState}; 8 | 9 | use crate::{custom::StatusCode, Problem}; 10 | 11 | /// Create a [`Problem`] instance with no available cause. 12 | /// 13 | /// This error is meant to be used when an _unrecoverable_ database error 14 | /// happens. Here, unrecoverable means errors that upper levels doesn't have any 15 | /// means to recover from other than retrying the operation or propagating it 16 | /// up. 17 | #[track_caller] 18 | fn sql_error(msg: M) -> Problem 19 | where 20 | M: std::error::Error + Send + Sync + 'static, 21 | { 22 | Problem::from_status(StatusCode::INTERNAL_SERVER_ERROR) 23 | .with_detail("An unexpected error occurred") 24 | .with_cause(msg) 25 | } 26 | 27 | #[cfg(feature = "diesel")] 28 | impl From for Problem { 29 | #[track_caller] 30 | fn from(err: PoolError) -> Self { 31 | sql_error(err) 32 | } 33 | } 34 | 35 | #[cfg(feature = "diesel")] 36 | impl From for Problem { 37 | #[track_caller] 38 | fn from(err: Error) -> Self { 39 | match err { 40 | Error::DatabaseError(kind, info) => match kind { 41 | DatabaseErrorKind::UniqueViolation => sql_error(UniqueViolation(info.into())), 42 | DatabaseErrorKind::ForeignKeyViolation => { 43 | sql_error(ForeignKeyViolation(info.into())) 44 | } 45 | DatabaseErrorKind::__Unknown if info.constraint_name().is_some() => { 46 | sql_error(CheckViolation(info.into())) 47 | } 48 | _ => sql_error(Error::DatabaseError(kind, info)), 49 | }, 50 | Error::NotFound => sql_error(NoRowsFound), 51 | err => sql_error(err), 52 | } 53 | } 54 | } 55 | 56 | #[cfg(feature = "tokio-postgres")] 57 | impl From for Problem { 58 | #[track_caller] 59 | fn from(err: PgError) -> Self { 60 | if let Some(db_err) = err.as_db_error() { 61 | match db_err.code().clone() { 62 | SqlState::UNIQUE_VIOLATION => sql_error(UniqueViolation(db_err.into())), 63 | SqlState::FOREIGN_KEY_VIOLATION => sql_error(ForeignKeyViolation(db_err.into())), 64 | SqlState::CHECK_VIOLATION => sql_error(CheckViolation(db_err.into())), 65 | _ => sql_error(err), 66 | } 67 | } else { 68 | sql_error(err) 69 | } 70 | } 71 | } 72 | 73 | #[cfg(feature = "sqlx")] 74 | impl From for Problem { 75 | #[track_caller] 76 | fn from(err: sqlx_core::Error) -> Self { 77 | if let Some(db_err) = err.as_database_error() { 78 | match db_err.kind() { 79 | sqlx_core::error::ErrorKind::UniqueViolation => { 80 | sql_error(UniqueViolation(db_err.into())) 81 | } 82 | sqlx_core::error::ErrorKind::ForeignKeyViolation => { 83 | sql_error(ForeignKeyViolation(db_err.into())) 84 | } 85 | sqlx_core::error::ErrorKind::CheckViolation => { 86 | sql_error(CheckViolation(db_err.into())) 87 | } 88 | _ => sql_error(err), 89 | } 90 | } else { 91 | sql_error(err) 92 | } 93 | } 94 | } 95 | 96 | /// A query returned no rows where it should be. 97 | /// 98 | /// This error only happens when a query expect only one row, otherwise, no rows 99 | /// is a valid return from it. 100 | /// 101 | /// Returns `500 - Internal Server Error` if not caught. We do this as we only 102 | /// emit such error when the code broke the invariant unexpectedly. If you 103 | /// expect that a query returns no rows, use [`OptionalExtension::optional`] or 104 | /// [`ProblemResultExt::optional`]. 105 | /// 106 | /// [`OptionalExtension::optional`]: diesel::result::OptionalExtension::optional 107 | /// [`ProblemResultExt::optional`]: crate::ext::ProblemResultExt::optional 108 | pub struct NoRowsFound; 109 | 110 | impl std::fmt::Debug for NoRowsFound { 111 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 112 | f.write_str("NoRowsFound") 113 | } 114 | } 115 | 116 | impl std::fmt::Display for NoRowsFound { 117 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 118 | f.write_str("No rows were found where one was expected") 119 | } 120 | } 121 | 122 | impl std::error::Error for NoRowsFound {} 123 | 124 | /// A query violated an `UNIQUE` constraint. 125 | /// 126 | /// Returns `500 - Internal Server Error` if not caught. We do this as the error 127 | /// is an invariant breakage that the develop MUST handle, either via `ON 128 | /// CONFLICT` clauses, parameter validations, or gracefully converting to 129 | /// [`Conflict`] when the client have _some_ way of solving the conflict (as 130 | /// stated in the [RFC 7231]). 131 | /// 132 | /// [`Conflict`]: crate::http::Conflict. 133 | /// [RFC 7231]: https://tools.ietf.org/html/rfc7231#section-6.5.8 134 | pub struct UniqueViolation(DbErrorInfo); 135 | 136 | impl UniqueViolation { 137 | /// The name of the column that the error is associated with. 138 | pub fn constraint_name(&self) -> Option<&str> { 139 | self.0.constraint_name.as_deref() 140 | } 141 | 142 | /// The primary human-readable error message. 143 | pub fn message(&self) -> &str { 144 | &self.0.message 145 | } 146 | 147 | /// Th name of th table the error is associated with. 148 | pub fn table_name(&self) -> Option<&str> { 149 | self.0.table_name.as_deref() 150 | } 151 | } 152 | 153 | impl std::fmt::Debug for UniqueViolation { 154 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 155 | f.debug_struct("UniqueViolation") 156 | .field("table_name", &self.table_name()) 157 | .field("constraint_name", &self.constraint_name()) 158 | .field("message", &self.message()) 159 | .finish() 160 | } 161 | } 162 | 163 | impl std::fmt::Display for UniqueViolation { 164 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 165 | f.write_str(self.message()) 166 | } 167 | } 168 | 169 | impl std::error::Error for UniqueViolation {} 170 | 171 | /// A query violated a `FOREIGN KEY` constraint. 172 | /// 173 | /// The same observations of [`UniqueViolation`] applies to this error. 174 | pub struct ForeignKeyViolation(DbErrorInfo); 175 | 176 | impl ForeignKeyViolation { 177 | /// The name of the column that the error is associated with. 178 | pub fn constraint_name(&self) -> Option<&str> { 179 | self.0.constraint_name.as_deref() 180 | } 181 | 182 | /// The primary human-readable error message. 183 | pub fn message(&self) -> &str { 184 | &self.0.message 185 | } 186 | 187 | /// Th name of th table the error is associated with. 188 | pub fn table_name(&self) -> Option<&str> { 189 | self.0.table_name.as_deref() 190 | } 191 | } 192 | 193 | impl std::fmt::Debug for ForeignKeyViolation { 194 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 195 | f.debug_struct("ForeignKeyViolation") 196 | .field("table_name", &self.table_name()) 197 | .field("constraint_name", &self.constraint_name()) 198 | .field("message", &self.message()) 199 | .finish() 200 | } 201 | } 202 | 203 | impl std::fmt::Display for ForeignKeyViolation { 204 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 205 | f.write_str(self.message()) 206 | } 207 | } 208 | 209 | impl std::error::Error for ForeignKeyViolation {} 210 | 211 | /// A query violated a `CHECK` constraint. 212 | /// 213 | /// The same observations of [`UniqueViolation`] applies to this error. 214 | pub struct CheckViolation(DbErrorInfo); 215 | 216 | impl CheckViolation { 217 | /// The name of the column that the error is associated with. 218 | pub fn constraint_name(&self) -> Option<&str> { 219 | self.0.constraint_name.as_deref() 220 | } 221 | 222 | /// The primary human-readable error message. 223 | pub fn message(&self) -> &str { 224 | &self.0.message 225 | } 226 | 227 | /// Th name of th table the error is associated with. 228 | pub fn table_name(&self) -> Option<&str> { 229 | self.0.table_name.as_deref() 230 | } 231 | } 232 | 233 | impl std::fmt::Debug for CheckViolation { 234 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 235 | f.debug_struct("CheckViolation") 236 | .field("table_name", &self.table_name()) 237 | .field("constraint_name", &self.constraint_name()) 238 | .field("message", &self.message()) 239 | .finish() 240 | } 241 | } 242 | 243 | impl std::fmt::Display for CheckViolation { 244 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 245 | f.write_str(self.message()) 246 | } 247 | } 248 | 249 | impl std::error::Error for CheckViolation {} 250 | 251 | struct DbErrorInfo { 252 | constraint_name: Option, 253 | message: String, 254 | table_name: Option, 255 | } 256 | 257 | #[cfg(feature = "diesel")] 258 | impl From> for DbErrorInfo { 259 | fn from(info: Box) -> Self { 260 | Self { 261 | constraint_name: info.constraint_name().map(String::from), 262 | message: info.message().to_string(), 263 | table_name: info.table_name().map(String::from), 264 | } 265 | } 266 | } 267 | 268 | #[cfg(feature = "tokio-postgres")] 269 | impl From<&'_ tokio_postgres::error::DbError> for DbErrorInfo { 270 | fn from(err: &'_ tokio_postgres::error::DbError) -> Self { 271 | Self { 272 | constraint_name: err.constraint().map(String::from), 273 | message: err.message().to_string(), 274 | table_name: err.table().map(String::from), 275 | } 276 | } 277 | } 278 | 279 | #[cfg(feature = "sqlx")] 280 | impl From<&'_ dyn sqlx_core::error::DatabaseError> for DbErrorInfo { 281 | fn from(err: &'_ dyn sqlx_core::error::DatabaseError) -> Self { 282 | Self { 283 | constraint_name: err.constraint().map(String::from), 284 | message: err.message().to_string(), 285 | table_name: err.table().map(String::from), 286 | } 287 | } 288 | } 289 | --------------------------------------------------------------------------------