Skip to main content

synapse_proxy/transform/
error_remap.rs

1//! `error_remap` response built-in: when the upstream status equals `when_status`,
2//! replace the body with a normalized `{error, detail?}` contract (status kept).
3
4use async_trait::async_trait;
5use axum::http::StatusCode;
6use serde_json::json;
7
8use crate::config::ErrorRemapSpec;
9use crate::context::ResolvedContext;
10
11use super::{ProxyResponse, ResponseTransform, TransformError};
12
13pub struct ErrorRemap {
14    when: StatusCode,
15    error: String,
16    detail: Option<String>,
17}
18
19impl ErrorRemap {
20    pub fn from_spec(spec: &ErrorRemapSpec) -> anyhow::Result<Self> {
21        let when = StatusCode::from_u16(spec.when_status).map_err(|_| {
22            anyhow::anyhow!("error_remap: invalid when_status {}", spec.when_status)
23        })?;
24        Ok(Self {
25            when,
26            error: spec.error.clone(),
27            detail: spec.detail.clone(),
28        })
29    }
30}
31
32#[async_trait]
33impl ResponseTransform for ErrorRemap {
34    async fn apply(
35        &self,
36        _ctx: &ResolvedContext,
37        resp: &mut ProxyResponse,
38    ) -> Result<(), TransformError> {
39        if resp.status == self.when {
40            let body = match &self.detail {
41                Some(d) => json!({ "error": self.error, "detail": d }),
42                None => json!({ "error": self.error }),
43            };
44            resp.replace_body(body);
45        }
46        Ok(())
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use super::*;
53    use crate::transform::ProxyResponse;
54    use axum::http::HeaderMap;
55    use std::collections::HashMap;
56
57    fn ctx() -> ResolvedContext {
58        crate::context::ContextStore::new(HashMap::new()).resolve()
59    }
60
61    #[tokio::test]
62    async fn remaps_matching_status() {
63        let er = ErrorRemap::from_spec(&ErrorRemapSpec {
64            when_status: 401,
65            error: "auth_expired".into(),
66            detail: None,
67        })
68        .unwrap();
69        let mut resp = ProxyResponse::new(StatusCode::UNAUTHORIZED, HeaderMap::new());
70        er.apply(&ctx(), &mut resp).await.unwrap();
71        assert_eq!(resp.status, StatusCode::UNAUTHORIZED); // status kept
72        assert_eq!(resp.replacement().unwrap()["error"], "auth_expired");
73    }
74
75    #[tokio::test]
76    async fn leaves_non_matching_status() {
77        let er = ErrorRemap::from_spec(&ErrorRemapSpec {
78            when_status: 401,
79            error: "auth_expired".into(),
80            detail: None,
81        })
82        .unwrap();
83        let mut resp = ProxyResponse::new(StatusCode::OK, HeaderMap::new());
84        er.apply(&ctx(), &mut resp).await.unwrap();
85        assert!(resp.replacement().is_none());
86    }
87
88    #[test]
89    fn from_spec_rejects_invalid_status() {
90        assert!(ErrorRemap::from_spec(&ErrorRemapSpec {
91            when_status: 9999,
92            error: "x".into(),
93            detail: None
94        })
95        .is_err());
96    }
97}