Skip to content

Handle any errors #26

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 14 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 10 additions & 10 deletions lambda-runtime-client/src/client.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use error::{ApiError, ErrorResponse, RuntimeApiError};
use error::{ApiError, ErrorResponse, ERROR_TYPE_UNHANDLED};
use hyper::{
client::HttpConnector,
header::{self, HeaderMap, HeaderValue},
Expand Down Expand Up @@ -250,7 +250,7 @@ impl RuntimeClient {
///
/// # Returns
/// A `Result` object containing a bool return value for the call or an `error::ApiError` instance.
pub fn event_error(&self, request_id: &str, e: &RuntimeApiError) -> Result<(), ApiError> {
pub fn event_error(&self, request_id: &str, e: ErrorResponse) -> Result<(), ApiError> {
let uri: Uri = format!(
"http://{}/{}/runtime/invocation/{}/error",
self.endpoint, RUNTIME_API_VERSION, request_id
Expand All @@ -259,9 +259,9 @@ impl RuntimeClient {
trace!(
"Posting error to runtime API for request {}: {}",
request_id,
e.to_response().error_message
e.error_message
);
let req = self.get_runtime_error_request(&uri, &e.to_response());
let req = self.get_runtime_error_request(&uri, e);

match self.http_client.request(req).wait() {
Ok(resp) => {
Expand Down Expand Up @@ -297,12 +297,12 @@ impl RuntimeClient {
/// # Panics
/// If it cannot send the init error. In this case we panic to force the runtime
/// to restart.
pub fn fail_init(&self, e: &RuntimeApiError) {
pub fn fail_init(&self, e: ErrorResponse) {
let uri: Uri = format!("http://{}/{}/runtime/init/error", self.endpoint, RUNTIME_API_VERSION)
.parse()
.expect("Could not generate Runtime URI");
error!("Calling fail_init Runtime API: {}", e.to_response().error_message);
let req = self.get_runtime_error_request(&uri, &e.to_response());
error!("Calling fail_init Runtime API: {}", e.error_message);
let req = self.get_runtime_error_request(&uri, e);

self.http_client
.request(req)
Expand Down Expand Up @@ -343,16 +343,16 @@ impl RuntimeClient {
.unwrap()
}

fn get_runtime_error_request(&self, uri: &Uri, e: &ErrorResponse) -> Request<Body> {
let body = serde_json::to_vec(e).expect("Could not turn error object into response JSON");
fn get_runtime_error_request(&self, uri: &Uri, e: ErrorResponse) -> Request<Body> {
let body = serde_json::to_vec(&e).expect("Could not turn error object into response JSON");
Request::builder()
.method(Method::POST)
.uri(uri.clone())
.header(
header::CONTENT_TYPE,
header::HeaderValue::from_static(API_ERROR_CONTENT_TYPE),
)
.header(RUNTIME_ERROR_HEADER, HeaderValue::from_static("RuntimeError")) // TODO: We should add this code to the error object.
.header(RUNTIME_ERROR_HEADER, HeaderValue::from_static(ERROR_TYPE_UNHANDLED))
.body(Body::from(body))
.unwrap()
}
Expand Down
99 changes: 54 additions & 45 deletions lambda-runtime-client/src/error.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
//! This module defines the `RuntimeApiError` trait that developers should implement
//! to send their custom errors to the AWS Lambda Runtime Client SDK. The module also
//! defines the `ApiError` type returned by the `RuntimeClient` implementations.
use std::{env, error::Error, fmt, io, num::ParseIntError, option::Option};
use std::{
env,
error::Error,
fmt::{self, Display},
io,
num::ParseIntError,
option::Option,
};

use backtrace;
use http::{header::ToStrError, uri::InvalidUri};
Expand All @@ -10,10 +17,14 @@ use serde_json;

/// Error type description for the `ErrorResponse` event. This type should be returned
/// for errors that were handled by the function code or framework.
pub const ERROR_TYPE_HANDLED: &str = "Handled";
#[allow(dead_code)]
pub(crate) const ERROR_TYPE_HANDLED: &str = "Handled";
/// Error type description for the `ErrorResponse` event. This type is used for unhandled,
/// unexpcted errors.
pub const ERROR_TYPE_UNHANDLED: &str = "Handled";
pub(crate) const ERROR_TYPE_UNHANDLED: &str = "Unhandled";
/// Error type for the error responses to the Runtime APIs. In the future, this library
/// should use a customer-generated error code
pub const RUNTIME_ERROR_TYPE: &str = "RustRuntimeError";

/// This object is used to generate requests to the Lambda Runtime APIs.
/// It is used for both the error response APIs and fail init calls.
Expand All @@ -36,6 +47,38 @@ pub struct ErrorResponse {
}

impl ErrorResponse {
/// Creates a new instance of the `ErrorResponse` object with the given parameters. If the
/// `RUST_BACKTRACE` env variable is `1` the `ErrorResponse` is populated with the backtrace
/// collected through the [`backtrace` craete](https://crates.io/crates/backtrace).
///
/// # Arguments
///
/// * `message` The error message to be returned to the APIs. Normally the error description()
/// * `err_type` The error type. Use the `ERROR_TYPE_HANDLED` and `ERROR_TYPE_UNHANDLED`.
/// * `code` A custom error code
///
/// # Return
/// A new instance of the `ErrorResponse` object.
fn new(message: String, err_type: String) -> ErrorResponse {
let mut err = ErrorResponse {
error_message: message,
error_type: err_type,
stack_trace: Option::default(),
};
let is_backtrace = env::var("RUST_BACKTRACE");
if is_backtrace.is_ok() && is_backtrace.unwrap() == "1" {
trace!("Begin backtrace collection");
let trace = Option::from(backtrace::Backtrace::new());
let trace_string = format!("{:?}", trace)
.lines()
.map(|s| s.to_string())
.collect::<Vec<String>>();
trace!("Completed backtrace collection");
err.stack_trace = Option::from(trace_string);
}
err
}

/// Creates a new `RuntimeError` object with the handled error type.
///
/// # Arguments
Expand All @@ -45,11 +88,7 @@ impl ErrorResponse {
/// # Return
/// A populated `RuntimeError` object that can be used with the Lambda Runtime API.
pub fn handled(message: String) -> ErrorResponse {
ErrorResponse {
error_message: message,
error_type: String::from(ERROR_TYPE_HANDLED),
stack_trace: Option::default(),
}
ErrorResponse::new(message, RUNTIME_ERROR_TYPE.to_owned())
}

/// Creates a new `RuntimeError` object with the unhandled error type.
Expand All @@ -61,33 +100,20 @@ impl ErrorResponse {
/// # Return
/// A populated `RuntimeError` object that can be used with the Lambda Runtime API.
pub fn unhandled(message: String) -> ErrorResponse {
ErrorResponse {
error_message: message,
error_type: String::from(ERROR_TYPE_UNHANDLED),
stack_trace: Option::default(),
}
ErrorResponse::new(message, RUNTIME_ERROR_TYPE.to_owned())
}
}

/// Custom errors for the framework should implement this trait. The client calls
/// the `to_response()` method automatically to produce an object that can be serialized
/// and sent to the Lambda Runtime APIs.
pub trait RuntimeApiError {
/// Creates a `RuntimeError` object for the current error. This is
/// then serialized and sent to the Lambda runtime APIs.
///
/// # Returns
/// A populated `RuntimeError` object.
fn to_response(&self) -> ErrorResponse;
impl<T: Display + Send + Sync> From<Box<T>> for ErrorResponse {
fn from(e: Box<T>) -> Self {
Self::handled(format!("{}", e))
}
}

/// Represents an error generated by the Lambda Runtime API client.
#[derive(Debug, Clone)]
pub struct ApiError {
msg: String,
/// The `Backtrace` object from the `backtrace` crate used to store
/// the stack trace of the error.
pub backtrace: Option<backtrace::Backtrace>,
/// Whether the current error is recoverable. If the error is not
/// recoverable a runtime should panic to force the Lambda service
/// to restart the execution environment.
Expand All @@ -96,16 +122,8 @@ pub struct ApiError {

impl ApiError {
pub(crate) fn new(description: &str) -> ApiError {
let mut trace: Option<backtrace::Backtrace> = None;
let is_backtrace = env::var("RUST_BACKTRACE");
if is_backtrace.is_ok() && is_backtrace.unwrap() == "1" {
trace!("Begin backtrace collection");
trace = Option::from(backtrace::Backtrace::new());
trace!("Completed backtrace collection");
}
ApiError {
msg: String::from(description),
backtrace: trace,
recoverable: true,
}
}
Expand Down Expand Up @@ -134,6 +152,8 @@ impl Error for ApiError {
None
}
}
unsafe impl Send for ApiError {}
unsafe impl Sync for ApiError {}

impl From<serde_json::Error> for ApiError {
fn from(e: serde_json::Error) -> Self {
Expand Down Expand Up @@ -170,14 +190,3 @@ impl From<io::Error> for ApiError {
ApiError::new(e.description())
}
}

impl RuntimeApiError for ApiError {
fn to_response(&self) -> ErrorResponse {
let backtrace = format!("{:?}", self.backtrace);
let trace_vec = backtrace.lines().map(|s| s.to_string()).collect::<Vec<String>>();
let mut err = ErrorResponse::unhandled(self.msg.clone());
err.stack_trace = Option::from(trace_vec);

err
}
}
4 changes: 3 additions & 1 deletion lambda-runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,7 @@ simple_logger = "^1"

[dev-dependencies]
hyper-tls = "^0.3"
simple-error = "^0.1"
failure = "^0.1"
rusoto_core = "^0.35"
rusoto_dynamodb = "^0.35"
rusoto_dynamodb = "^0.35"
6 changes: 4 additions & 2 deletions lambda-runtime/examples/basic.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
extern crate lambda_runtime as lambda;
extern crate log;
extern crate serde_derive;
extern crate simple_error;
extern crate simple_logger;

use lambda::{error::HandlerError, lambda};
use lambda::{lambda, HandlerError};
use log::error;
use serde_derive::{Deserialize, Serialize};
use simple_error::bail;
use std::error::Error;

#[derive(Deserialize)]
Expand All @@ -29,7 +31,7 @@ fn main() -> Result<(), Box<dyn Error>> {
fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
if e.first_name == "" {
error!("Empty first name in request {}", c.aws_request_id);
return Err(c.new_error("Empty first name"));
bail!("Empty first name");
}

Ok(CustomOutput {
Expand Down
66 changes: 66 additions & 0 deletions lambda-runtime/examples/custom_error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
extern crate lambda_runtime as lambda;
extern crate log;
extern crate serde_derive;
extern crate simple_logger;

use lambda::{lambda, HandlerError};
use log::error;
use serde_derive::{Deserialize, Serialize};
use std::{error::Error, fmt};

#[derive(Debug)]
struct CustomError {
msg: String,
}

impl fmt::Display for CustomError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.msg)
}
}

impl Error for CustomError {}

impl CustomError {
fn new(message: &str) -> CustomError {
CustomError {
msg: message.to_owned(),
}
}
}

#[derive(Deserialize)]
struct CustomEvent {
#[serde(rename = "firstName")]
first_name: String,
age: String,
}

#[derive(Serialize)]
struct CustomOutput {
message: String,
}

fn main() -> Result<(), Box<dyn Error>> {
simple_logger::init_with_level(log::Level::Debug).unwrap();
lambda!(my_handler);

Ok(())
}

fn my_handler(e: CustomEvent, c: lambda::Context) -> Result<CustomOutput, HandlerError> {
if e.first_name == "" {
error!("Empty first name in request {}", c.aws_request_id);
// in this case, we explicitly initialize and box our custom error type.
// the HandlerError type is an alias to Box<dyn Error>/
return Err(CustomError::new("Empty first name").into());
}

// For errors simply want to return, because the HandlerError is an alias to any
// generic error type, we can propapgate with the standard "?" syntax.
let _age_num: u8 = e.age.parse()?;

Ok(CustomOutput {
message: format!("Hello, {}!", e.first_name),
})
}
Loading