'Rust actix-web middleware modifying request data
Is there a recommended way of modifying the request received on actix-web.
Is there an example on how to modify I am looking for way to add data to the request object and have it available for processing by downstream middlewares and handlers.
The Middleware documentation at "https://actix.rs/docs/middleware/" says the following "Actix-web’s middleware system allows us to add additional behavior to request/response processing. Middleware can hook into an incoming request process, enabling us to modify requests as well as halt request processing to return a response early."
The page doesn't have an example on how to modify the Request.
Let take the code below (obtained from "https://actix.rs/docs/middleware/"), what would be the code to somehow add data to the request?
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_service::{Service, Transform};
use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error};
use futures::future::{ok, Ready};
use futures::Future;
// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
// next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
pub struct SayHi;
// Middleware factory is `Transform` trait from actix-service crate
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S> for SayHi
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = SayHiMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(SayHiMiddleware { service })
}
}
pub struct SayHiMiddleware<S> {
service: S,
}
impl<S, B> Service for SayHiMiddleware<S>
where
S: Service<Request = ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&mut self, req: ServiceRequest) -> Self::Future {
println!("Hi from start. You requested: {}", req.path());
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
println!("Hi from response");
Ok(res)
})
}
}
Solution 1:[1]
I was just looking at how to do this as well, disappointing that they talk about it in their docs but do not show any examples.
I found that you can either edit the headers or extensions as follows, I'm not sure how to edit it otherwise.
- Grab the headers from the Service request,
let headers = req.headers_mut()
- Take the HeaderMap and you can from there either insert, remove, clear, etc
- Insert example:
headers.insert(HeaderName::from_lowercase(b"content-length").unwrap(), HeaderValue::from_static("hello"));
- Get the extensions for the request
let extensions = req.extensions_mut()
- Add to the extension
extensions.insert("foo".to_string());
- One liner:
req.extensions_mut().insert("foo".to_string());
- Get that extension later:
req.extensions().get::<String>()
Solution 2:[2]
It's certainly possible to modify a request and the associated response from middleware. Here is a brief example(this works with Actix v4):
use x_contrib::{actix_http, actix_web, body, futures, log, HttpMessage, HttpResponseBuilder};
use std::cell::RefCell;
use log::Level;
use std::pin::Pin;
use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform};
use actix_web::Error;
use futures::future::{ok, Ready};
use futures::task::{Context, Poll};
use std::future::Future;
use std::rc::Rc;
use std::str;
use crate::middleware::utility::{ApiMiddlewareUtility, MiddlewareUtility};
use crate::request;
use x_common::prelude::*;
use x_common::settings;
use x_contrib::actix_http::h1::Payload;
use x_contrib::body::{EitherBody, MessageBody};
use x_contrib::futures::future::err;
use x_contrib::futures::StreamExt;
use x_contrib::web::{Buf, BytesMut};
pub struct LogEvents;
impl<S: 'static, B> Transform<S, ServiceRequest> for LogEvents
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Transform = LogEventsMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(LogEventsMiddleware {
service: Rc::new(RefCell::new(service)),
})
}
}
pub struct LogEventsMiddleware<S> {
service: Rc<RefCell<S>>,
}
impl<S: 'static, B> Service<ServiceRequest> for LogEventsMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: MessageBody + 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
actix_web::dev::forward_ready!(service);
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let svc = self.service.clone();
let log_level = settings::get_setting("log_level").unwrap_or("info".to_owned());
Box::pin(async move {
match log_level.as_str() {
"debug" | "trace" | "info" => {
let route = req.path().to_owned();
/* we only process requests that are json */
if !MiddlewareUtility::is_json_request(&req) {
let res: ServiceResponse = svc.call(req).await?.map_into_boxed_body();
return Ok(res.map_into_right_body());
}
/* extract and log the request */
let mut request_body = BytesMut::new();
while let Some(chunk) = req.take_payload().next().await {
request_body.extend_from_slice(&chunk?);
}
match str::from_utf8(&request_body.to_vec().as_slice()) {
Ok(str) => {
/* identify routes that we will redact the body from,
these are items that contain sensitive information we do not want to log
*/
match route.as_str() {
"/x/protected_endpoint" => {
tracing::info!({ body = "Redacted" }, "HTTP Request");
}
_ => {
tracing::info!({body = %str}, "HTTP Request");
}
}
}
Err(_) => {}
};
let (payload_sender, mut orig_payload) = Payload::create(true);
orig_payload.unread_data(request_body.freeze());
req.set_payload(actix_http::Payload::from(orig_payload));
/* extract and log the response */
let res: ServiceResponse = svc.call(req).await?.map_into_boxed_body();
if !MiddlewareUtility::is_json_response(&res) {
return Ok(res.map_into_right_body());
}
let res_status = res.status().clone();
let res_headers = res.headers().clone();
let new_request = res.request().clone();
let body_bytes = body::to_bytes(res.into_body()).await?;
match str::from_utf8(&body_bytes) {
Ok(str) => {
tracing::info!({body = %str}, "HTTP Response");
str
}
Err(_) => "Unknown",
};
/* build an identical response */
let mut new_response = HttpResponseBuilder::new(res_status);
for (header_name, header_value) in res_headers {
new_response.insert_header((header_name.as_str(), header_value));
}
let new_response = new_response.body(body_bytes.to_vec());
Ok(ServiceResponse::new(
new_request,
new_response.map_into_right_body(),
))
}
_ => {
let res: ServiceResponse = svc.call(req).await?.map_into_boxed_body();
Ok(res.map_into_right_body())
}
}
})
}
}
This particular example integrates with tracing_actix_web, a fantastic telemetry tool and logs the request/response to jaeger if the request/response is json.
One thing to note, as far as I know, once you read out the request, you have to reassemble it, same with the response. Hence what the example is doing.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Dharman |
Solution 2 |