mkit_server/
storage_error.rs1use std::fmt;
15
16use crate::error::ServerError;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum StorageOp {
24 BlobBinding,
26 BlobPut,
28 BlobGet,
30 BlobRead,
32 BlobHead,
34 MultipartSession,
36 MultipartPart,
38 MetaBinding,
41 MetaStub,
43 MetaRequest,
45 MetaCall,
48 MetaDecode,
50 RequestSerialize,
53 SqlExec,
55 FsIo,
57}
58
59impl StorageOp {
60 pub const ALL: [Self; 15] = [
62 Self::BlobBinding,
63 Self::BlobPut,
64 Self::BlobGet,
65 Self::BlobRead,
66 Self::BlobHead,
67 Self::MultipartSession,
68 Self::MultipartPart,
69 Self::MetaBinding,
70 Self::MetaStub,
71 Self::MetaRequest,
72 Self::MetaCall,
73 Self::MetaDecode,
74 Self::RequestSerialize,
75 Self::SqlExec,
76 Self::FsIo,
77 ];
78
79 const fn label(self) -> &'static str {
81 match self {
82 Self::BlobBinding => "blob store binding",
83 Self::BlobPut => "blob put",
84 Self::BlobGet => "blob get",
85 Self::BlobRead => "blob read",
86 Self::BlobHead => "blob head",
87 Self::MultipartSession => "multipart session",
88 Self::MultipartPart => "multipart part",
89 Self::MetaBinding => "metadata store binding",
90 Self::MetaStub => "metadata store stub",
91 Self::MetaRequest => "metadata store request build",
92 Self::MetaCall => "metadata store call",
93 Self::MetaDecode => "metadata store decode",
94 Self::RequestSerialize => "request serialize",
95 Self::SqlExec => "sql exec",
96 Self::FsIo => "filesystem io",
97 }
98 }
99
100 #[must_use]
103 pub const fn public_message(self) -> &'static str {
104 match self {
105 Self::BlobBinding
106 | Self::BlobPut
107 | Self::BlobGet
108 | Self::BlobRead
109 | Self::BlobHead
110 | Self::MultipartSession
111 | Self::MultipartPart => "object storage request failed",
112 Self::MetaBinding
113 | Self::MetaStub
114 | Self::MetaRequest
115 | Self::MetaCall
116 | Self::MetaDecode
117 | Self::SqlExec => "ref store request failed",
118 Self::RequestSerialize => "internal request encoding failed",
119 Self::FsIo => "storage request failed",
120 }
121 }
122}
123
124#[must_use]
132pub fn describe_and_map(op: StorageOp, detail: impl fmt::Display) -> (String, ServerError) {
133 let log_line = format!("{}: {detail}", op.label());
134 let err = ServerError::internal(op.public_message(), &log_line);
135 (log_line, err)
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::error::Code;
142
143 #[test]
146 fn every_op_maps_to_one_of_the_fixed_generic_messages() {
147 const ALLOWED: &[&str] = &[
148 "object storage request failed",
149 "ref store request failed",
150 "internal request encoding failed",
151 "storage request failed",
152 ];
153 for op in StorageOp::ALL {
154 assert!(
155 ALLOWED.contains(&op.public_message()),
156 "{op:?} has an unexpected client message: {}",
157 op.public_message()
158 );
159 }
160 }
161
162 #[test]
165 fn simulated_storage_failure_is_logged_but_never_reaches_the_client() {
166 let raw_detail = "R2Error: bucket 'mkit-prod-packs-9c1e' access denied \
167 for account 4f8e21a9-c3b2-4d11-9e77-1a2b3c4d5e6f \
168 (JsValue: TypeError at fetch_r2_binding@worker.js:1842)";
169
170 for op in StorageOp::ALL {
171 let (log_line, err) = describe_and_map(op, raw_detail);
172 assert!(
173 log_line.contains(raw_detail),
174 "{op:?}: log line dropped the real error detail: {log_line:?}"
175 );
176 assert_eq!(err.code(), Code::Internal);
177 assert_eq!(err.public_message(), op.public_message());
178 assert_eq!(err.log_detail(), Some(log_line.as_str()));
179 let shown = [format!("{err}"), format!("{err:?}"), format!("{err:#?}")];
180 for client in [err.public_message()]
181 .into_iter()
182 .chain(shown.iter().map(String::as_str))
183 {
184 for secret in [
185 raw_detail,
186 "mkit-prod-packs-9c1e",
187 "4f8e21a9-c3b2-4d11-9e77-1a2b3c4d5e6f",
188 "worker.js",
189 ] {
190 assert!(
191 !client.contains(secret),
192 "{op:?} leaked {secret:?}: {client}"
193 );
194 }
195 }
196 }
197 }
198}