DEV Community

Cover image for Rust: actix-web endpoints which accept both application/x-www-form-urlencoded and application/json content types.
Be Hai Nguyen
Be Hai Nguyen

Posted on

Rust: actix-web endpoints which accept both application/x-www-form-urlencoded and application/json content types.

We're implementing a login process for our actix-web learning application. We undertake some general updates to get ready to support login. We then implement a new /api/login route, which supports both application/x-www-form-urlencoded and application/json content types. In this post, we only implement deserialising the submitted request data, then echo some response. We also add a login page via route /ui/login.

🚀 Please note, complete code for this post can be downloaded from GitHub with:

git clone -b v0.5.0 https://github.com/behai-nguyen/rust_web_01.git
Enter fullscreen mode Exit fullscreen mode

The actix-web learning application mentioned above has been discussed in the following four (4) previous posts:

  1. Rust web application: MySQL server, sqlx, actix-web and tera.
  2. Rust: learning actix-web middleware 01.
  3. Rust: retrofit integration tests to an existing actix-web application.
  4. Rust: adding actix-session and actix-identity to an existing actix-web application.

The code we're developing in this post is a continuation of the code from the fourth post above. 🚀 To get the code of this fourth post, please use the following command:

git clone -b v0.4.0 https://github.com/behai-nguyen/rust_web_01.git
Enter fullscreen mode Exit fullscreen mode

-- Note the tag v0.4.0.

As already mentioned in the introduction above, in this post, our main focus of the login process is deserialising both application/x-www-form-urlencoded and application/json into a struct ready to support login. I struggle with this issue a little, I document it as part of my Rust learning journey.


This post introduces a few new modules, some MySQL migration scripts, and a new login HTML page. The updated directory layout for the project is in the screenshot below:

095-01.png

Table of contents


❶ Update Rust to the latest version. At the time of this post, the latest version is 1.75.0. The command to update:

▶️<code>Windows 10:</code> rustup update
▶️<code>Ubuntu 22.10:</code> $ rustup update
Enter fullscreen mode Exit fullscreen mode

We've taken CORS into account when we started out this project in this first post.

I'm not quite certain what'd happened, but all of a sudden, it just rejects requests with message Origin is not allowed to make this request.

-- Browsers have been updated, perhaps?

Failing to troubleshoot the problem, and seeing that actix-cors is at version 0.7.0. I update it.

-- It does not work with Rust version 1.74.0. This new version of actix-cors seems to fix the above request rejection issue.


❷ Update the employees table, adding new fields email and password.

Using the migration tool SQLx CLI, which we've covered in Rust SQLx CLI: database migration with MySQL and PostgreSQL, to update the employees table.

While inside the new directory migrations/mysql/, see project directory layout above, create empty migration files 99999999999999_emp_email_pwd.up.sql and 99999999999999_emp_email_pwd.down.sql using the command:

▶️<code>Windows 10:</code> sqlx migrate add -r emp_email_pwd
▶️<code>Ubuntu 22.10:</code> $ sqlx migrate add -r emp_email_pwd
Enter fullscreen mode Exit fullscreen mode

Populate the two script files with what we would like to do. Please see their contents on GitHub. To apply, run the below command, it'll take a little while to complete:

▶️<code>Windows 10:</code> sqlx migrate add -r emp_email_pwd
▶️<code>Ubuntu 22.10:</code> $ sqlx migrate add -r emp_email_pwd
Enter fullscreen mode Exit fullscreen mode


❸ Update src/models.rs to manage new fields employees.email and employees.password.

If we run cargo test now, all integration tests should fail. All integration tests eventually call to get_employees(...), which does a select * from employees.... Since the two new fields've been added to a specific order, field indexes in get_employees(...) are out of order.

Module src/models.rs gets the following updates:

  1. pub email: String field added to struct Employee.
  2. pub async fn get_employees(...) updated to read Employee.email field. Other fields' indexes also get updated.
  3. New pub struct EmployeeLogin.
  4. New pub async fn select_employee(...), which optionally selects an employee base on exact email match.
  5. New pub struct LoginSuccess.
  6. Add "email": "siamak.bernardeschi.67115@gmail.com" to existing tests.

Please see the updated src/models.rs on GitHub. The documentation should be sufficient to help reading the code.


❹ New module src/auth_handlers.rs, where new login routes /ui/login and /api/login are implemented.

http://0.0.0.0:5000/ui/login is a GET route, which just returns the login.html page as HTML.

http://0.0.0.0:5000/api/login is a POST route. This is effectively the application login handler.

💥 This http://0.0.0.0:5000/api/login route is the main focus of this post:

-- Its handler method accepts both application/x-www-form-urlencoded and application/json content types, and deserialises the byte stream to struct EmployeeLogin mentioned above.

💥 Please also note that, as already mentioned, in this post, the login process does not do login, if successfully deserialised the submitted data, it'd just echo a confirmation response in the format of the request content type. If failed to deserialise, it'd send back a JSON response which has an error code and a text message.

Examples of valid submitted data for each content type:

✔️ Content type: application/x-www-form-urlencoded; data: email=chirstian.koblick.10004@gmail.com&password=password.

✔️ Content type: application/json; data: {"email": "chirstian.koblick.10004@gmail.com", "password": "password"}.

Content of src/auth_handlers.rs

#[post("/login")]
pub async fn login(
    request: HttpRequest,
    body: Bytes
) -> HttpResponse {
...
    // Attempts to extract -- deserialising -- request body into EmployeeLogin.
    let api_status = extract_employee_login(&body, request.content_type());
    // Failed to deserialise request body. Returns the error as is.
    if api_status.is_err() {
        return HttpResponse::Ok()
            .content_type(ContentType::json())
            .body(serde_json::to_string(&api_status.err().unwrap()).unwrap());
    }

    // Succeeded to deserialise request body.
    let emp_login: EmployeeLogin = api_status.unwrap();
... 
Enter fullscreen mode Exit fullscreen mode

Note the second parameter body, which is actix_web::web::Bytes, this is the byte stream presentation of the request body.

As an extractor, actix_web::web::Bytes has been mentioned in section Type-safe information extraction | Other. We're providing our own implementation to do the deserialisation, method extract_employee_login(...) in new module src/helper/endpoint.rs.

Content of src/helper/endpoint.rs

pub fn extract_employee_login(
    body: &Bytes, 
    content_type: &str
) -> Result<EmployeeLogin, ApiStatus> {
...
    extractors.push(Extractor { 
        content_type: mime::APPLICATION_WWW_FORM_URLENCODED.to_string(), 
        handler: |body: &Bytes| -> Result<EmployeeLogin, ApiStatus> {
            match from_bytes::<EmployeeLogin>(&body.to_owned().to_vec()) {
                Ok(e) => Ok(e),
                Err(e) => Err(ApiStatus::new(err_code_500()).set_text(&e.to_string()))
            }
        }
    });
...
    extractors.push(Extractor {
        content_type: mime::APPLICATION_JSON.to_string(),
        handler: |body: &Bytes| -> Result<EmployeeLogin, ApiStatus> {
            // From https://stackoverflow.com/a/67340858
            match serde_json::from_slice(&body.to_owned()) {
                Ok(e) => Ok(e),
                Err(e) => Err(ApiStatus::new(err_code_500()).set_text(&e.to_string()))
            }
        }
    });
Enter fullscreen mode Exit fullscreen mode

For application/x-www-form-urlencoded content type, we call method serde_html_form::from_bytes(...) from (new) crate serde_html_form to deserialise the byte stream to EmployeeLogin.

-- Cargo.toml has been updated to include crate serde_html_form.

And for application/json content type, we call to serde_json::from_slice(...) from the already included serde_json crate to do the work.

These're the essential details of the code. The rest is fairly straightforward, and there's also sufficient documentation to aid the reading of the code.

💥 Please also note that there're also some more new modules, such as src/bh_libs/api_status.rs and src/helper/messages.rs, they're very small, self-explanatory and have sufficient documentation where appropriate.


❺ Register new login routes /ui/login and /api/login.

Updated src/lib.rs:

pub async fn run(listener: TcpListener) -> Result<Server, std::io::Error> {
...
            .service(
                web::scope("/ui")
                    .service(handlers::employees_html1)
                    .service(handlers::employees_html2)
                    .service(auth_handlers::login_page)
                    // .service(auth_handlers::home_page),
            )
            .service(
                web::scope("/api")
                    .service(auth_handlers::login)
            )
            .service(
                web::resource("/helloemployee/{last_name}/{first_name}")
                    .wrap(middleware::SayHi)
                    .route(web::get().to(handlers::hi_first_employee_found))
            )
...         
Enter fullscreen mode Exit fullscreen mode


❻ The last addition, the new templates/auth/login.html.

Please note, this login page has only HTML. There is no CSS at all. It looks like a dog's breakfast, but it does work. There is no client-side validations either.

The Login button POSTs login requests to http://0.0.0.0:5000/api/login, the content type then is application/x-www-form-urlencoded.

For application/json content type, we can use Testfully. (We could also write our own AJAX requests to test.)


❼ As this is not yet the final version of the login process, we're not writing any integration tests for it yet. We'll do so in due course...


⓵ For the time being, we've written some new code and their associated unit tests. We have also written some documentation examples. The full test with the command cargo test should have all tests pass.


⓶ Manual tests of the new routes.

In the following two successful tests, I run the application server on an Ubuntu 22.10 machine, and run both the login page and Testfully on Windows 10.

Test application/x-www-form-urlencoded submission via login page:

095-02.png
095-03.png

Test application/json submission using Testfully:

095-04.png
095-05.png

In this failure test, I run the application server and Testfully on Windows 10. The submitted application/json data does not have an email field:

095-06.png
095-07.png

It's been an interesting exercise for me. My understanding of Rust's improved a little. I hope you find the information in this post useful. Thank you for reading and stay safe as always.

✿✿✿

Feature image source:

Top comments (0)