chore(backend): route wiring, error mapping, and a crossbeam-epoch bump
Cargo.lock moves crossbeam-epoch to 0.9.20, clearing RUSTSEC-2026-0204. Targeted rather than a broad `cargo update` across 406 crates, which is not a change to make days before a live event. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,27 @@ pub enum AppError {
|
||||
/// (banned user, quota): the queued blob is kept and retried if the host reopens,
|
||||
/// instead of being purged like a genuinely-terminal rejection.
|
||||
UploadsLocked(String),
|
||||
/// The gallery has been RELEASED — the keepsake was snapshotted, so a late upload could
|
||||
/// never appear in it. Mechanically this is still reversible (a host reopen clears
|
||||
/// `export_released_at` and bumps the epoch), which is why the blob must still be kept.
|
||||
///
|
||||
/// Distinct from `UploadsLocked` because the two differ in *expectation*, and the client's
|
||||
/// retry policy has to differ with them. A closed event is a pause the host means to undo;
|
||||
/// a released gallery is the end of the event, and nobody reopens it. Under one shared code
|
||||
/// the queue kept auto-retrying a released event forever — re-streaming a multi-megabyte
|
||||
/// photo over cellular on every budget refill, for a request whose answer will not change,
|
||||
/// while telling the guest to tap a camera button that 403s. `gallery_released` lets the
|
||||
/// client park the item visibly and wait for an actual `event-opened` instead of guessing.
|
||||
GalleryReleased(String),
|
||||
/// The uploader is banned. A 403 like `Forbidden`, but tagged `user_banned` so the client
|
||||
/// keeps the queued blob instead of purging it.
|
||||
///
|
||||
/// A ban is reversible — `unban_user` exists, and the host's own confirm copy promises the
|
||||
/// photos come back — but the client classified the generic `forbidden` code as permanent,
|
||||
/// deleted the blob from IndexedDB, and moved the row to `blocked`, which has no retry
|
||||
/// button. So an unban could restore everything except the photos that were in flight when
|
||||
/// the ban landed, and a ban issued by mistake destroyed them with no way back.
|
||||
UserBanned(String),
|
||||
NotFound(String),
|
||||
Conflict(String),
|
||||
/// Second field: optional retry-after seconds to include in the response.
|
||||
@@ -35,6 +56,8 @@ impl AppError {
|
||||
Self::Unauthorized(_) => (StatusCode::UNAUTHORIZED, "unauthorized"),
|
||||
Self::Forbidden(_) => (StatusCode::FORBIDDEN, "forbidden"),
|
||||
Self::UploadsLocked(_) => (StatusCode::FORBIDDEN, "uploads_locked"),
|
||||
Self::GalleryReleased(_) => (StatusCode::FORBIDDEN, "gallery_released"),
|
||||
Self::UserBanned(_) => (StatusCode::FORBIDDEN, "user_banned"),
|
||||
Self::NotFound(_) => (StatusCode::NOT_FOUND, "not_found"),
|
||||
Self::Conflict(_) => (StatusCode::CONFLICT, "conflict"),
|
||||
Self::TooManyRequests(..) => (StatusCode::TOO_MANY_REQUESTS, "too_many_requests"),
|
||||
@@ -52,6 +75,8 @@ impl AppError {
|
||||
| Self::Unauthorized(msg)
|
||||
| Self::Forbidden(msg)
|
||||
| Self::UploadsLocked(msg)
|
||||
| Self::GalleryReleased(msg)
|
||||
| Self::UserBanned(msg)
|
||||
| Self::NotFound(msg)
|
||||
| Self::Conflict(msg) => msg.clone(),
|
||||
Self::TooManyRequests(msg, _) => msg.clone(),
|
||||
@@ -190,9 +215,8 @@ mod tests {
|
||||
AppError::ServiceUnavailable("busy".into(), Some(3)),
|
||||
] {
|
||||
let expected = match &err {
|
||||
AppError::TooManyRequests(_, Some(s)) | AppError::ServiceUnavailable(_, Some(s)) => {
|
||||
s.to_string()
|
||||
}
|
||||
AppError::TooManyRequests(_, Some(s))
|
||||
| AppError::ServiceUnavailable(_, Some(s)) => s.to_string(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let resp = err.into_response();
|
||||
|
||||
Reference in New Issue
Block a user