Axum Query Parameter is only deserialized up to the first parameter and all the rest are left as None in lambda

153 Views Asked by At

I am using Axum rust to handle Query parameters in the URL within the runtime. The issue that I can't figure out is that the program will only deserialize the first query parameter. If there are two or more, then the second and third query parameters will be read as null.

#![warn(clippy::cargo, clippy::unwrap_used)]
mod config;
mod models;
mod route_handlers;
mod web_server;
use aws_sdk_dynamodb::Client;
use config::{make_config, Opt};
use lambda_runtime::tower::ServiceBuilder;
use std::env::set_var;
use tower_http::add_extension::AddExtensionLayer;
use web_server::get_router;

const REGION: &str = "us-east-1";

#[tokio::main]
async fn main() -> Result<(), lambda_http::Error> {
    // AWS Runtime can ignore Stage Name passed from json event
    // https://github.com/awslabs/aws-lambda-rust-runtime/issues/782
    set_var("AWS_LAMBDA_HTTP_IGNORE_STAGE_IN_PATH", "true");

    // required to enable CloudWatch error logging by the runtime
    tracing_subscriber::fmt()
        .with_max_level(tracing::Level::INFO)
        // disable printing the name of the module in every log line.
        .with_target(false)
        // disabling time is handy because CloudWatch will add the ingestion time.
        .without_time()
        .init();

    // creating a dynamo db client for the router to use
    let config = make_config(Opt {
        region: Some(REGION.to_owned()),
        verbose: true,
    })
    .await;
    let client = std::sync::Arc::new(Client::new(
        &config.expect("Could not unwrap aws config for dynamodb client"),
    ));

    let router = get_router();

    // adding the client into the router as an extension layer
    let router = ServiceBuilder::new()
        .layer(AddExtensionLayer::new(client))
        .service(router);

    lambda_http::run(router).await
}

pub fn get_router() -> axum::Router {
    // Sets the axum router to produce only ERROR level logs. The handlers can produce their own level of logs.
    const LOG_LEVEL: Level = Level::ERROR;

    // With Routing, order does matter, so put the more specific first and then more generic routes later
    Router::new()
        .route("/conversations", get(list_conversations));

pub async fn list_conversations(
    Extension(db_client): Extension<Arc<DynamoDbClient>>,
    Query(params): Query<PaginationRequest>,
) -> WebResult {
    info!("params: {:?}", params);
    let client = db_client.clone();
    scan_items::<Conversation>(&client, DBTable::Conversation, params).await
}

#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct PaginationRequest {
    // The hash key
    pub next_key_a: Option<String>,

    // The sort key
    pub next_key_b: Option<String>,

    // if no pagination should be used for the client, and all data should be returned
    pub fetch_all: Option<bool>,
}

When I invoke the api gateway, with next_key_a first, it will be read but next_key_b will be None. If i switch the query parameters, they will then change from Some to None

Calling https://abc.execute-api.us-east-1.amazonaws.com/test-stage/conversations?next_key_b=7e95e561-3b3f-4a82-954f-fb37f8c0f257&next_key_a=053baf43-ac9c-4754-a94a-de982c169204 will cause next_key_b to be Some() and next_key_a to be None.

Then calling https://abc.execute-api.us-east-1.amazonaws.com/test-stage/conversations?next_key_a=7e95e561-3b3f-4a82-954f-fb37f8c0f257&next_key_b=053baf43-ac9c-4754-a94a-de982c169204 will be the opposite.

I think it is something that happens before the payload from API Gateway hits the axum web server because I've tried a bunch of unit tests to make sure this struct can handle this from the url:

 #[test]
    fn test_pagination_request_full() {
        let uri: Uri = "http://example.com/path?next_key_a=hello&next_key_b=world&fetch_all=true"
            .parse()
            .unwrap();
        let result: Query<PaginationRequest> = Query::try_from_uri(&uri).unwrap();
        assert_eq!(result.0.next_key_a, Some(String::from("hello")));
        assert_eq!(result.0.next_key_b, Some(String::from("world")));
        assert_eq!(result.0.fetch_all, Some(true));
    }

    #[test]
    fn test_pagination_request_full_swapped() {
        let uri: Uri = "http://example.com/path?next_key_b=world&fetch_all=true&next_key_a=hello"
            .parse()
            .unwrap();
        let result: Query<PaginationRequest> = Query::try_from_uri(&uri).unwrap();
        assert_eq!(result.0.next_key_a, Some(String::from("hello")));
        assert_eq!(result.0.next_key_b, Some(String::from("world")));
        assert_eq!(result.0.fetch_all, Some(true));
    }

    #[test]
    fn test_pagination_request_partial() {
        let uri: Uri = "http://example.com/path?next_key_a=hello&fetch_all=true"
            .parse()
            .unwrap();
        let result: Query<PaginationRequest> = Query::try_from_uri(&uri).unwrap();
        assert_eq!(result.0.next_key_a, Some(String::from("hello")));
        assert_eq!(result.0.next_key_b, None);
        assert_eq!(result.0.fetch_all, Some(true));
    }

These tests show that Axum can handle the Queries if they are passed into the framework correctly. So I think lambda runtime is working differently than I expected.

0

There are 0 best solutions below