Continuing with our actix-web learning application, we implement global extractor error handlers for both application/json and application/x-www-form-urlencoded data. This enhances the robustness of the code. Subsequently, we refactor the login data extraction process to leverage the global extractor error handlers.

πŸ¦€ Index of the Complete Series.

099-feature-image.jpg
Rust: actix-web global extractor error handlers.

πŸš€ Please note, complete code for this post can be downloaded from GitHub with:

git clone -b v0.9.0 https://github.com/behai-nguyen/rust_web_01.git

The actix-web learning application mentioned above has been discussed in the following eight 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.
  5. Rust: actix-web endpoints which accept both application/x-www-form-urlencoded and application/json content types.
  6. Rust: simple actix-web email-password login and request authentication using middleware.
  7. Rust: actix-web get SSL/HTTPS for localhost.
  8. Rust: actix-web CORS, Cookies and AJAX calls.

The code we’re developing in this post is a continuation of the code from the eighth post above. πŸš€ To get the code of this eighth post, please use the following command:

git clone -b v0.8.0 https://github.com/behai-nguyen/rust_web_01.git

– Note the tag v0.8.0.

❢ We are not adding any new files to the project; it remains the same as in the seventh post. We are only making changes to some modules.

.
β”œβ”€β”€ Cargo.toml β˜…
β”œβ”€β”€ README.md β˜…
β”œβ”€β”€ src
β”‚ β”œβ”€β”€ auth_handlers.rs β˜…
β”‚ β”œβ”€β”€ handlers.rs β˜…
β”‚ β”œβ”€β”€ helper
β”‚ β”‚ β”œβ”€β”€ app_utils.rs β˜…
β”‚ β”‚ β”œβ”€β”€ endpoint.rs β˜…
β”‚ β”‚ └── messages.rs β˜…
β”‚ β”œβ”€β”€ helper.rs β˜…
β”‚ └── lib.rs β˜…
└── tests
    β”œβ”€β”€ test_auth_handlers.rs β˜…
    └── test_handlers.rs β˜…

– Please note, those marked with β˜… are updated, and those marked with β˜† are new.

❷ Currently, the application does not handle extraction errors for both application/json and application/x-www-form-urlencoded data in data-related routes.

πŸš€ As a reminder, we have the following existing data-related routes. Briefly:

  • Route https://0.0.0.0:5000/data/employees accepts application/json. For example {"last_name": "%chi", "first_name": "%ak"}.
  • Route https://0.0.0.0:5000/ui/employees accepts application/x-www-form-urlencoded. For example last_name=%chi&first_name=%ak.

Unlike the data-related routes, the login route https://0.0.0.0:5000/api/login currently implements a custom extractor that also handles extraction errors. Please refer to the sections Implementations of Routes /ui/login and /api/login and How the Email-Password Login Process Works in previous posts for more details. πŸ’₯ We will refactor this implementation to eliminate the custom extractor and fully leverage the global extractor error handlers that we are going to implement.

Let’s demonstrate some unhandled extraction errors for both content types.

πŸš€ Please note that the ajax_test.html page is used in the examples below.

β“΅ application/json content type. First, we make an invalid submission with empty data. Then, we submit data with an invalid field name:


The above screenshots indicate that there is some implicit default extraction error handling in place: the response status code is 400 for BAD REQUEST, and the response text contains the actual extraction error message.

πŸ’₯ However, this behavior is not consistent with the existing implementation for the https://0.0.0.0:5000/api/login route, where an extraction error always results in a JSON serialisation of ApiStatus with a code of 400 for BAD REQUEST, and the message containing the exact extraction error. For more details, refer to the current implementation of pub fn extract_employee_login(body: &Bytes, content_type: &str) -> Result<EmployeeLogin, ApiStatus> It’s worth noting that, as mentioned earlier, we are also refactoring this custom extractor while retaining its current handling of extraction errors.

β“Ά application/x-www-form-urlencoded content type. Similar to the previous example, we also submit two invalid requests: one with empty data and another with data containing an invalid field name:


❸ Implementing β€œglobal extractor error handlers” for application/json and application/x-www-form-urlencoded data.

This involves configuring extractor configurations provided by the actix-web crate, namely JsonConfig and FormConfig, respectively. We can define custom error handlers for each content type using their error_handler(...) method.

In our context, we refer to these custom error handlers as β€œglobal extractor error handlers”.

Based on the documentation, we implement the functions fn json_config() -> web::JsonConfig and fn form_config() -> web::FormConfig, and then register them according to the official example.

The key part is the error_handler(...) function within both extractor configurations:

...
        .error_handler(|err, _req| {
            let err_str: String = String::from(err.to_string());
            error::InternalError::from_response(err, 
                make_api_status_response(StatusCode::BAD_REQUEST, &err_str, None)).into()
        })
...

Here, err_str represents the actual extraction error message.

We utilise the function
pub fn make_api_status_response( status_code: StatusCode, message: &str, session_id: Option<String>) -> HttpResponse to construct a response, which is a JSON serialisation of ApiStatus.

We can verify the effectiveness of the global extractor error handlers by repeating the previous two examples.

β“΅ application/json content type:


The screenshots confirm that we receive the expected response, which contrasts the example prior to refactoring.

β“Ά application/x-www-form-urlencoded content type:


We get the expected response. This is the example before refactoring.

β“· Let’s try another example via Postman:


When an extraction error occurs, the response is a JSON serialisation of ApiStatus. When a request to route https://0.0.0.0:5000/ui/employees is successful, the response is HTML. (As a reminder, we need to set the request authorization header to something, for example, chirstian.koblick.10004@gmail.com.)

❹ Integration tests for data-related routes.

To ensure that the global extractor error handlers function correctly, we need tests to verify their behavior.

In tests/test_handlers.rs, we’ve implemented four failed extraction tests, each ending with _error_empty and _error_missing_field.

These tests closely resemble the examples shown previously. The code for the new tests is similar to existing ones, so we won’t walk through it as they are self-explanatory.

πŸ’₯ In the new tests, take note of the error messages: "Content type error" and "Content type error."!

❺ Refactoring the login data extraction process.

In the fifth post, Rust: actix-web endpoints which accept both application/x-www-form-urlencoded and application/json content types, we implemented the custom extractor function pub fn extract_employee_login(body: &Bytes, content_type: &str) -> Result<EmployeeLogin, ApiStatus> which accepts both application/x-www-form-urlencoded and application/json content types, and deserialises the byte stream to the EmployeeLogin struct.

This function is currently functional. As mentioned previously, we intend to refactor the code while retaining its extraction error handling behaviors, which are now available automatically due to the introduction of global extractor error handlers.

We are eliminating this helper function and instead using the enum Either, which provides a mechanism for trying two extractors: a primary and a fallback.

In src/auth_handlers.rs, the login function, the endpoint handler for route /api/login, is updated as follows:

#[post("/login")]
pub async fn login(
    request: HttpRequest,
    app_state: web::Data<super::AppState>,
    body: Either<web::Json<EmployeeLogin>, web::Form<EmployeeLogin>>
) -> HttpResponse {
    let submitted_login  = match body {
        Either::Left(json) => json.into_inner(),
        Either::Right(form) => form.into_inner(),
    };
...	

The last parameter and the return type have changed. The parameter body is now an enum Either, which is the focal point of this refactoring. The extraction process is more elegant, and we are taking advantage of a built-in feature, which should be well-tested.

The global extractor error handlers enforce the same validations on the submitted data as the previous custom extractor helper function.

Please note the previous return type of this function:

#[post("/login")]
pub async fn login(
    request: HttpRequest,
    app_state: web::Data<super::AppState>,
    body: Bytes
) -> Either<impl Responder, HttpResponse> {
...

There are other minor changes throughout the function, but they are self-explanatory.

Let’s observe the refactored login code in action.

β“΅ application/json content type. Two invalid requests and one valid request:


β“Ά application/x-www-form-urlencoded content type. Two invalid requests and one valid request:


β“· application/x-www-form-urlencoded content type. Using Postman. Two invalid requests and one valid request:


β“Έ application/x-www-form-urlencoded content type. Using the application’s login page, first log in with an invalid email, then log in again with a valid email and password.


❻ Integration tests for invalid login data.

These tests should have been written earlier, immediately after completing the login functionalities.

In the test module, tests/test_auth_handlers.rs, we’ve added four failed extraction tests, denoted by functions ending with _error_empty and _error_missing_field.

❼ We have reached the conclusion of this post. I don’t feel that implementing the function extract_employee_login was a waste of time. Through this process, I’ve gained valuable insights into Rust.

As for the next post for this project, I’m not yet sure what it will entail πŸ˜‚β€¦ There are still several functionalities I would like to implement. I’ll let my intuition guide me in deciding the topic for the next post.

Thank you for reading, and I hope you find the information in this post useful. Stay safe, as always.

✿✿✿

Feature image source:

πŸ¦€ Index of the Complete Series.