Skip to main content

actix_web/middleware/
default_headers.rs

1//! For middleware documentation, see [`DefaultHeaders`].
2
3use std::{
4    future::Future,
5    marker::PhantomData,
6    pin::Pin,
7    rc::Rc,
8    task::{Context, Poll},
9};
10
11use actix_http::error::HttpError;
12#[expect(deprecated, reason = "preserve the public ready future type")]
13use actix_utils::future::{ready, Ready};
14use futures_core::ready;
15use pin_project_lite::pin_project;
16
17use crate::{
18    dev::{Service, Transform},
19    http::header::{HeaderMap, HeaderName, HeaderValue, TryIntoHeaderPair, CONTENT_TYPE},
20    service::{ServiceRequest, ServiceResponse},
21    Error,
22};
23
24/// Middleware for setting default response headers.
25///
26/// Headers with the same key that are already set in a response will *not* be overwritten.
27///
28/// # Examples
29/// ```
30/// use actix_web::{web, http, middleware, App, HttpResponse};
31///
32/// let app = App::new()
33///     .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
34///     .service(
35///         web::resource("/test")
36///             .route(web::get().to(|| HttpResponse::Ok()))
37///             .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed()))
38///     );
39/// ```
40#[derive(Debug, Clone, Default)]
41pub struct DefaultHeaders {
42    inner: Rc<Inner>,
43}
44
45#[derive(Debug, Default)]
46struct Inner {
47    headers: HeaderMap,
48}
49
50impl DefaultHeaders {
51    /// Constructs an empty `DefaultHeaders` middleware.
52    #[inline]
53    pub fn new() -> DefaultHeaders {
54        DefaultHeaders::default()
55    }
56
57    /// Adds a header to the default set.
58    ///
59    /// # Panics
60    /// Panics when resolved header name or value is invalid.
61    #[allow(clippy::should_implement_trait)]
62    pub fn add(mut self, header: impl TryIntoHeaderPair) -> Self {
63        // standard header terminology `insert` or `append` for this method would make the behavior
64        // of this middleware less obvious since it only adds the headers if they are not present
65
66        match header.try_into_pair() {
67            Ok((key, value)) => Rc::get_mut(&mut self.inner)
68                .expect("All default headers must be added before cloning.")
69                .headers
70                .append(key, value),
71            Err(err) => panic!("Invalid header: {}", err.into()),
72        }
73
74        self
75    }
76
77    #[doc(hidden)]
78    #[deprecated(
79        since = "4.0.0",
80        note = "Prefer `.add((key, value))`. Will be removed in v5."
81    )]
82    pub fn header<K, V>(self, key: K, value: V) -> Self
83    where
84        HeaderName: TryFrom<K>,
85        <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
86        HeaderValue: TryFrom<V>,
87        <HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
88    {
89        self.add((
90            HeaderName::try_from(key)
91                .map_err(Into::into)
92                .expect("Invalid header name"),
93            HeaderValue::try_from(value)
94                .map_err(Into::into)
95                .expect("Invalid header value"),
96        ))
97    }
98
99    /// Adds a default *Content-Type* header if response does not contain one.
100    ///
101    /// Default is `application/octet-stream`.
102    pub fn add_content_type(self) -> Self {
103        #[allow(clippy::declare_interior_mutable_const)]
104        const HV_MIME: HeaderValue = HeaderValue::from_static("application/octet-stream");
105        self.add((CONTENT_TYPE, HV_MIME))
106    }
107}
108
109#[expect(deprecated, reason = "preserve the public ready future type")]
110impl<S, B> Transform<S, ServiceRequest> for DefaultHeaders
111where
112    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
113    S::Future: 'static,
114{
115    type Response = ServiceResponse<B>;
116    type Error = Error;
117    type Transform = DefaultHeadersMiddleware<S>;
118    type InitError = ();
119    type Future = Ready<Result<Self::Transform, Self::InitError>>;
120
121    fn new_transform(&self, service: S) -> Self::Future {
122        ready(Ok(DefaultHeadersMiddleware {
123            service,
124            inner: Rc::clone(&self.inner),
125        }))
126    }
127}
128
129pub struct DefaultHeadersMiddleware<S> {
130    service: S,
131    inner: Rc<Inner>,
132}
133
134impl<S, B> Service<ServiceRequest> for DefaultHeadersMiddleware<S>
135where
136    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
137    S::Future: 'static,
138{
139    type Response = ServiceResponse<B>;
140    type Error = Error;
141    type Future = DefaultHeaderFuture<S, B>;
142
143    actix_service::forward_ready!(service);
144
145    fn call(&self, req: ServiceRequest) -> Self::Future {
146        let inner = Rc::clone(&self.inner);
147        let fut = self.service.call(req);
148
149        DefaultHeaderFuture {
150            fut,
151            inner,
152            _body: PhantomData,
153        }
154    }
155}
156
157pin_project! {
158    pub struct DefaultHeaderFuture<S: Service<ServiceRequest>, B> {
159        #[pin]
160        fut: S::Future,
161        inner: Rc<Inner>,
162        _body: PhantomData<B>,
163    }
164}
165
166impl<S, B> Future for DefaultHeaderFuture<S, B>
167where
168    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
169{
170    type Output = <S::Future as Future>::Output;
171
172    #[allow(clippy::borrow_interior_mutable_const)]
173    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
174        let this = self.project();
175        let mut res = ready!(this.fut.poll(cx))?;
176
177        // set response headers
178        for (key, value) in this.inner.headers.iter() {
179            if !res.headers().contains_key(key) {
180                res.headers_mut().insert(key.clone(), value.clone());
181            }
182        }
183
184        Poll::Ready(Ok(res))
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use std::future::ready;
191
192    use actix_service::IntoService;
193
194    use super::*;
195    use crate::{
196        test::{self, TestRequest},
197        HttpResponse,
198    };
199
200    #[actix_rt::test]
201    async fn adding_default_headers() {
202        let mw = DefaultHeaders::new()
203            .add(("X-TEST", "0001"))
204            .add(("X-TEST-TWO", HeaderValue::from_static("123")))
205            .new_transform(test::ok_service())
206            .await
207            .unwrap();
208
209        let req = TestRequest::default().to_srv_request();
210        let res = mw.call(req).await.unwrap();
211        assert_eq!(res.headers().get("x-test").unwrap(), "0001");
212        assert_eq!(res.headers().get("x-test-two").unwrap(), "123");
213    }
214
215    #[actix_rt::test]
216    async fn no_override_existing() {
217        let req = TestRequest::default().to_srv_request();
218        let srv = |req: ServiceRequest| {
219            ready(Ok(req.into_response(
220                HttpResponse::Ok()
221                    .insert_header((CONTENT_TYPE, "0002"))
222                    .finish(),
223            )))
224        };
225        let mw = DefaultHeaders::new()
226            .add((CONTENT_TYPE, "0001"))
227            .new_transform(srv.into_service())
228            .await
229            .unwrap();
230        let resp = mw.call(req).await.unwrap();
231        assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002");
232    }
233
234    #[actix_rt::test]
235    async fn adding_content_type() {
236        let mw = DefaultHeaders::new()
237            .add_content_type()
238            .new_transform(test::ok_service())
239            .await
240            .unwrap();
241
242        let req = TestRequest::default().to_srv_request();
243        let resp = mw.call(req).await.unwrap();
244        assert_eq!(
245            resp.headers().get(CONTENT_TYPE).unwrap(),
246            "application/octet-stream"
247        );
248    }
249
250    #[test]
251    #[should_panic]
252    fn invalid_header_name() {
253        DefaultHeaders::new().add((":", "hello"));
254    }
255
256    #[test]
257    #[should_panic]
258    fn invalid_header_value() {
259        DefaultHeaders::new().add(("x-test", "\n"));
260    }
261}