use std::fmt;
use crate::error::ServerError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum StorageOp {
BlobBinding,
BlobPut,
BlobGet,
BlobRead,
BlobHead,
MultipartSession,
MultipartPart,
MetaBinding,
MetaStub,
MetaRequest,
MetaCall,
MetaDecode,
RequestSerialize,
SqlExec,
FsIo,
}
impl StorageOp {
pub const ALL: [Self; 15] = [
Self::BlobBinding,
Self::BlobPut,
Self::BlobGet,
Self::BlobRead,
Self::BlobHead,
Self::MultipartSession,
Self::MultipartPart,
Self::MetaBinding,
Self::MetaStub,
Self::MetaRequest,
Self::MetaCall,
Self::MetaDecode,
Self::RequestSerialize,
Self::SqlExec,
Self::FsIo,
];
const fn label(self) -> &'static str {
match self {
Self::BlobBinding => "blob store binding",
Self::BlobPut => "blob put",
Self::BlobGet => "blob get",
Self::BlobRead => "blob read",
Self::BlobHead => "blob head",
Self::MultipartSession => "multipart session",
Self::MultipartPart => "multipart part",
Self::MetaBinding => "metadata store binding",
Self::MetaStub => "metadata store stub",
Self::MetaRequest => "metadata store request build",
Self::MetaCall => "metadata store call",
Self::MetaDecode => "metadata store decode",
Self::RequestSerialize => "request serialize",
Self::SqlExec => "sql exec",
Self::FsIo => "filesystem io",
}
}
#[must_use]
pub const fn public_message(self) -> &'static str {
match self {
Self::BlobBinding
| Self::BlobPut
| Self::BlobGet
| Self::BlobRead
| Self::BlobHead
| Self::MultipartSession
| Self::MultipartPart => "object storage request failed",
Self::MetaBinding
| Self::MetaStub
| Self::MetaRequest
| Self::MetaCall
| Self::MetaDecode
| Self::SqlExec => "ref store request failed",
Self::RequestSerialize => "internal request encoding failed",
Self::FsIo => "storage request failed",
}
}
}
#[must_use]
pub fn describe_and_map(op: StorageOp, detail: impl fmt::Display) -> (String, ServerError) {
let log_line = format!("{}: {detail}", op.label());
let err = ServerError::internal(op.public_message(), &log_line);
(log_line, err)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::Code;
#[test]
fn every_op_maps_to_one_of_the_fixed_generic_messages() {
const ALLOWED: &[&str] = &[
"object storage request failed",
"ref store request failed",
"internal request encoding failed",
"storage request failed",
];
for op in StorageOp::ALL {
assert!(
ALLOWED.contains(&op.public_message()),
"{op:?} has an unexpected client message: {}",
op.public_message()
);
}
}
#[test]
fn simulated_storage_failure_is_logged_but_never_reaches_the_client() {
let raw_detail = "R2Error: bucket 'mkit-prod-packs-9c1e' access denied \
for account 4f8e21a9-c3b2-4d11-9e77-1a2b3c4d5e6f \
(JsValue: TypeError at fetch_r2_binding@worker.js:1842)";
for op in StorageOp::ALL {
let (log_line, err) = describe_and_map(op, raw_detail);
assert!(
log_line.contains(raw_detail),
"{op:?}: log line dropped the real error detail: {log_line:?}"
);
assert_eq!(err.code(), Code::Internal);
assert_eq!(err.public_message(), op.public_message());
assert_eq!(err.log_detail(), Some(log_line.as_str()));
let shown = [format!("{err}"), format!("{err:?}"), format!("{err:#?}")];
for client in [err.public_message()]
.into_iter()
.chain(shown.iter().map(String::as_str))
{
for secret in [
raw_detail,
"mkit-prod-packs-9c1e",
"4f8e21a9-c3b2-4d11-9e77-1a2b3c4d5e6f",
"worker.js",
] {
assert!(
!client.contains(secret),
"{op:?} leaked {secret:?}: {client}"
);
}
}
}
}
}