Error handling and conditional chaining of Actix actors - rust

This is my first attempt at writing a small webservice with rust, using actix-web.
The code below is a request handler that is intended to do three things, insert an entry in the database, send an email if that db call was successful, and then return a json payload as the response.
data.dal (database call) and data.email_service are references to Actors.
The issue: is I am unable to capture the error returned by data.dal. Any attempt to reconfigure the below code seems to give me an error stating the compiler wasn't able to find a conversion from Actix Mailbox to [Type].
Is there an alternate/better way to rewrite this? Basically when the request is issued, I'd like to be able to call Actor A. And if the result from A is Ok then call Actor B. If the results from both are okay return a JSON payload. If either A or B return an error (can have different error types), return an custom error message.
pub fn register_email(
invitation: Json<EmailInvitationInput>,
data: web::Data<AppState>,
) -> impl Future<Item=HttpResponse, Error=Error> {
let m = dal::queries::CreateEmailInvitation { email: invitation.email.clone() };
data.dal.send(m)
.from_err()
.and_then(move |res| {
let invite = res.unwrap();
let email_input = email::SendLoginLink {
from: "from_email".to_string(),
to: "to_email".to_string(),
};
data.email_service.send(email_input)
.from_err()
.and_then(move |res| match res {
Ok(_) => {
Ok(HttpResponse::Ok().json(EmailInvitationOutput { expires_at: invite.expires_at }))
}
Err(err) => {
debug!("{:#?}", err);
Ok(ServiceError::InternalServerError.error_response())
}
})
})
}

What I usually do is to have an Error type that agglomerates all different errors, the coercion to this type can be achieved implicitly by declaring the appropriate From implementations and what you are doing from_err() but here I am being explicit:
I haven't tested this code snippet but this is how I have done it in projects I'm working on that use Actix:
data.dal.send(m)
.map_err(Error::Mailbox)
.and_then(|res| res.map_err(Error::Service))
.and_then(move |invite| {
let email_input = email::SendLoginLink {
from: "from_email".to_string(),
to: "to_email".to_string(),
};
data.email_service.send(email_input)
.map_err(Error::Mailbox)
.and_then(|res| res.map_err(Error::Service))
.and_then(move |res| HttpResponse::Ok().json(EmailInvitationOutput { expires_at: invite.expires_at }))
})
.or_else(|err| {
debug!("{:#?}", err);
ServiceError::InternalServerError.error_response()
})
(I'm assuming ServiceError implements IntoFuture just like HttpResponse does)

Related

How to properly handle a tokio::try_join! if one of the tasks panics and cleanly abort?

For the two async functions that I am passing to my try_join!(), let's say there's 3 ways that they could panic.
I'm trying to use a set_hook to catch the errors but I'm not sure how to do a match statement on the panics so I can display a custom error message for each of the ways that they can panic. It looks like set_hook takes a Box(Any) (?), so I was wondering if there was a way to check the type of Error. Basically I just don't want to do regex on the ErrString.
I'm also not sure what the best way to abort the runtime within each match branch. I'm currently using std::process::exit(0).
code looks like:
set_hook(Box::new(|panic_info| {
println!("Thread panicked! {}", panic_info);
// std::process::exit(0);
}));
let (result1, result2) = tokio::try_join!(func1, func2); // code that could panic
I want to be able to do something like
set_hook(Box::new(|panic_info| {
match panic_info {
panic_type_1 => { println!("X was invalid, please try using valid X") }
panic_type_2 => { println!("Y was invalid, please try using valid Y") }
panic_type_3 => { println!("Z was invalid, please try using valid Z") }
_ => { println!("Something else happened: {}", panic_info) }
}
}));
let (result1, result2) = tokio::try_join!(func1, func2); // code that could panic
Don't bother with set_hook. The future that tokio::task::spawn* returns resolves to a Result with a JoinError type, which has a [try_]into_panic to get the boxed object that was passed to panic.
The panic message is stored as a Box<dyn Any> which has tons of methods for downcasting it into various types.

How can I accept invalid or self-signed SSL certificates in Rust futures reqwest?

My code looks like the following:
let fetches = futures::stream::iter(
hosts.into_iter().map(|url| {
async move {
match reqwest::get(&url).await {
// Ok and Err statements here!
}
But, the problem here is that it gives an error for URLs with invalid or self-signed SSL certificate. So, I tried to do the following:
let fetches = futures::stream::iter(
hosts.into_iter().map(|url| {
async move {
match reqwest::Client::builder().danger_accept_invalid_certs(true).build().unwrap().get(&url).await {
// Ok and Err statements here!
}
When I try to build it with Cargo, it says "error[E0277]: `RequestBuilder` is not a future".
So, how can I make my code accept invalid certificates?
Unlike the top-level get() function, which returns a Response, the Client::get() method which you call in the second snippet, returns a RequestBuilder, which you must send() to actually communicate.
Adding the missing send() allows the code to compile (playgropund):
fn main() {
let hosts: Vec<String> = vec![];
let fetches = futures::stream::iter(hosts.into_iter().map(|url| async move {
match reqwest::Client::builder()
.danger_accept_invalid_certs(true)
.build()
.unwrap()
.get(&url)
.send()
.await
{
Ok(x) => x,
Err(x) => panic!(),
}
}));
}

Getting current path when handling rejections

I'd like to know how it would be possible to get HTTP path in Warp's rejection handler? I've got the following rejection method:
pub(crate) async fn handle(err: Rejection) -> Result<impl Reply, Infallible> {
let response = if err.is_not_found() {
HttpApiProblem::with_title_and_type_from_status(StatusCode::NOT_FOUND)
} else if let Some(e) = err.find::<warp::filters::body::BodyDeserializeError>() {
HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail(format!("{}", e))
} else if let Some(e) = err.find::<Error>() {
handle_request_error(e)
} else if let Some(e) = err.find::<warp::reject::MethodNotAllowed>() {
HttpApiProblem::with_title_and_type_from_status(StatusCode::METHOD_NOT_ALLOWED)
.set_detail(format!("{}", e))
} else {
error!("handle_rejection catch all: {:?}", err);
HttpApiProblem::with_title_and_type_from_status(StatusCode::INTERNAL_SERVER_ERROR)
};
Ok(response.to_hyper_response())
}
For instance, I'd call curl localhost:1234/this-aint-valid-path/123 and would like to have access to /this-aint-valid-path/123 for logging purposes as well as returning this as part of the error response.
It loos like the method err.is_not_found() checks whether the reason matches Reason::NotFound, which is an enumeration variant with no parameters. Rejection structs have no additional metadata beyond their reason, so the code in your question cannot be modified to solve your problem. It is however possible to create a custom reason with whatever metadata you want. The method you're looking for to create that Rejection object is called custom, and the docs for it can be found here.

Actix-Web runtime error: thread 'actix-rt:worker:2' panicked at 'AuthenticationMiddleware was called already'

I have an actix-web server using HttpAuthentication middleware to authenticate all requests. The server runs fine and responds correctly to most requests, but occasionally certain requests trigger the error:
thread 'actix-rt:worker:2' panicked at 'AuthenticationMiddleware was called already
A request to the same endpoint will only trigger the error some of the time, so I am not sure what the root cause is.
My main() function (with only the relevant code included is:
#[actix_rt::main]
async fn main() -> std::io::Result<()> {
// Some configuration code here
HttpServer::new(move || {
App::new()
.wrap(
HttpAuthentication::basic(validator)
)
// other code here
})
.bind(ip)?
.run()
.await
}
The validator function passed as the process_fn argument to HttpAuthentication::basic is:
async fn validator(
req: ServiceRequest,
credentials: BasicAuth,
) -> Result<ServiceRequest, Error> {
let config = req.app_data::<Config>()
.map(|data| data.get_ref().clone())
.unwrap_or_else(Default::default)
.scope("urn:example:channel=HBO&urn:example:rating=G,PG-13");
let username = env::var("USERNAME")
.expect("USERNAME must be set");
let password = env::var("PASSWORD")
.expect("USERNAME must be set");
if credentials.user_id().deref() == username {
match credentials.password() {
Some(pass) => {
if pass.deref() == password {
Ok(req)
} else {
Err(AuthenticationError::from(config).into())
}
}
None => Err(AuthenticationError::from(config).into())
}
} else {
Err(AuthenticationError::from(config).into())
}
}
This function essentially is just checking for the validity of the basic authentication username and password sent in the request. As I understand it, this should be wrapping every endpoint on the server and only allowing authenticated requests through.
What I do not understand is why I am getting this runtime error. Does anyone have any ideas as to why this is happening?

How to check the authorization header using Warp?

I'm building a graphql api with Rust and Warp. I've looked through the docs, but I have still not figured out how to chain the filters, especially for checking the authorization in request header.
let context_extractor = warp::any()
// this code rejects all request which doesn't contain the authorization in header
// I'd like to make to check if authorization in header
.and(warp::header::<String>("authorization"))
.map(|token: String| -> Context {
let token_data = match verify_jwt(token) {
Ok(t) => t,
Err(_) => return Context { user_id: 0 },
};
Context {
user_id: token_data.claims.user_id,
}
});
let handle_request = move |context: Context,
request: juniper::http::GraphQLRequest|
-> Result<Vec<u8>, serde_json::Error> {
serde_json::to_vec(&request.execute(&schema, &context))
};
warp::post2()
.and(warp::path(path.into()))
.and(context_extractor)
.and(warp::body::json())
.map(handle_request)
.map(build_response)
.boxed()
This is my part of code. It works fine, but there is one problem. I've set up one route context_extractor with .and(warp::header::<String>("authorization"), then it rejects all requests which doesn't contain authorization in header.
How can I make
if request header has a authorization in header, then return Context with the proper user_id
if not, return Context with user_id: 0?
I've found the solution in github issues of Warp.
here is a small snippet.
let context_extractor = warp::any().and(
warp::header::<String>("authorization")
.map(|token: String| -> Context {
let token_data = match verify_jwt(token) {
Ok(t) => t,
Err(_) => return Context { user_id: 0 },
};
Context {
user_id: token_data.claims.user_id,
}
})
.or(warp::any().map(|| Context { user_id: 0 }))
.unify(),
);

Resources