I would like to modify the res variable's body before returning, but since it is of type B (a generic) the compiler won't allow me to convert it to Bytes and use things like String::from_utf8(body.to_vec()). How can I modify the value of the response's body? Again it is an unconstrained Generic of type B.
impl<S, B> Service<ServiceRequest> for ReqAppenderMiddlewareExecutor<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(next_service);
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let fut = self.next_service.clone();
Box::pin(async move {
let body_original = req.extract::<Bytes>().await.unwrap();
if req.content_type() == ContentType::json().0 {
let body_str = String::from_utf8(body_original.to_vec());
match body_str {
Ok(str) => {
let req_body_json: Result<RequestBodyJson, serde_json::Error> = serde_json::from_str(&str);
match req_body_json {
Ok(mut rbj) => {
rbj.msg = format!("{}. how are you?", rbj.msg);
let new_rbj_result = serde_json::to_string(&rbj);
let new_rbj_str = new_rbj_result.unwrap().clone();
let body_final = Bytes::from(new_rbj_str.clone());
req.set_payload(bytes_to_payload(body_final));
},
Err(_) => println!("Not of type RequestBodyJson, continuing")
};
},
Err(_) => println!("Payload not string, continuing")
};
}
let res = fut.call(req).await?;
Ok(res)
})
}
}
If you need to return a response with a different body type than that returned by the service you call in your middleware, actix provides the EitherBody enum for that. See i.e. this topic:
In the implementation you can I see I extracted the req as a Bytes and then got the utf8 string from it in order to modify that string.
I would like to do something similar to the res variable's body before returning it. In other words I need access to res' body to change it.
Actix web's middleware requires a secondary Trait impl called Transform which uses the other impl and that one will fail since it expects an unconstrained B not a bound B of B: Bytes.
According to App::wrap, the only constraint put on B is that it implements MessageBody, which is true for Bytes, String, etc. Transform itself does not have a generic parameter that would match B per se, it is only part of its Response type, i.e. Response = ServiceResponse<B>, so Transform can't require anything from B. The only constraint on the generic parameter B is coming from App::wrap, which requires that Transform::Response = ServiceResponse<B>, where B: MessageBody.
By adding B: 'static + MessageBody + Clone to the impl for Transform and Service I was able to get this code in without the squiggles (needed to clone body so that res would not be taken and I could use it later to call res.response_mut). However after adding Clone, now my App instance wrap is complaining.
error[E0277]: the trait bound BoxBody: Clone is not satisfied
--> section_1/finish/middleware/src/main.rs:15:19
|
15 | .wrap(ReqAppenderMiddlewareBuilder)
| ---- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait Clone is not implemented for BoxBody
| |
| required by a bound introduced by this call
|
= help: the trait Transform<S, ServiceRequest> is implemented for ReqAppenderMiddlewareBuilder
note: required for ReqAppenderMiddlewareBuilder to implement Transform<actix_web::app_service::AppRouting, ServiceRequest>
--> section_1/finish/middleware/src/req_middleware.rs:20:12
impl<S, B> Service<ServiceRequest> for ReqAppenderMiddlewareExecutor<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static + MessageBody + Clone
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(next_service);
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let fut = self.next_service.clone();
Box::pin(async move {
let body_original = req.extract::<Bytes>().await.unwrap();
if req.content_type() == ContentType::json().0 {
let body_str = String::from_utf8(body_original.to_vec());
match body_str {
Ok(str) => {
let req_body_json: Result<RequestMessage, serde_json::Error> = serde_json::from_str(&str);
match req_body_json {
Ok(mut rbj) => {
rbj.msg = format!("{}. I modified the request.", rbj.msg);
let new_rbj_result = serde_json::to_string(&rbj);
let new_rbj_str = new_rbj_result.unwrap();
let body_final = Bytes::from(new_rbj_str);
req.set_payload(bytes_to_payload(body_final));
},
Err(_) => println!("Not of type RequestMessage, continuing")
};
},
Err(_) => println!("Payload not string, continuing")
};
}
let mut res = fut.call(req).await?;
if res.headers().contains_key("content-type") {
let body = res.response().body().clone();
let body_bytes_result = body.try_into_bytes();
match body_bytes_result {
Ok(body_bytes) => {
let body_str = String::from_utf8(body_bytes.to_vec());
match body_str {
Ok(str) => {
let body_obj: Result<ResponseMessage, serde_json::Error> = serde_json::from_str(&str);
match body_obj {
Ok(mut body_obj) => {
body_obj.msg = format!("{}. I modified the response.", body_obj.msg);
let new_body_obj_result = serde_json::to_string(&body_obj);
let new_body_obj_str = new_body_obj_result.unwrap();
let body_final = Bytes::from(new_body_obj_str);
res.response_mut().set_body(body_final);
},
Err(_) => println!("Not of type ResponseMessage, continuing")
};
},
Err(_) => println!("To string failed")
}
},
Err(_) => println!("Payload not string, continuing")
};
}
Ok(res)
})
}
}
As BoxBody does not implement Clone, I think you have to consume res and construct a new ServiceResponse from res's headers and the newly created body.
Thanks for your help. I'm almost there but having one more issue. When I build a new ServiceResponse it has to be of type B, but when I build it I get ServiceResponse. This is my error.
body_obj.msg = format!("{}. I modified the response.", body_obj.msg);
let new_body_obj_result = serde_json::to_string(&body_obj);
let new_body_obj_str = new_body_obj_result.unwrap();
let body_final = Bytes::from(new_body_obj_str); // also tried getting rid of this Bytes conversion and
// making my body_final object type impl MessageBody but that did nothing
let resp = HttpResponse::build(status)
.content_type("application/json")
.body(body_final);
Ok(req.into_response(resp)) // req.into_response returns ServiceResponse<BoxBody>
Thing that I don't get here is in my function signature B is constrained as B: 'static + MessageBody. And I can see in the docs that BoxBody does an impl of MessageBody. So why would BoxBody not be accepted as B?
This associated type is the reason why you need to return ServiceResponse<B>. Try changing it to ServiceResponse<BoxBody>, as you only ever return ServiceResponse<BoxBody> AFAICT. If you return either ServiceResponse<B> or ServiceResponse<BoxBody>, try returning ServiceResponse<EitherBody<B>> instead.
If this won't work and you still need help, would you mind posting a minimal reproducible example (including your Transform implementation)?
The concrete types for generic parameters are chosen by the caller, and it could choose e.g. B=Vec<u8>, and of course BoxBody is different than Vec<u8>.