#routing #router #http-router #squall

squall-router

HTTP router with path parameters extraction

2 unstable releases

0.2.2 Jan 15, 2022
0.2.1 Jan 14, 2022
0.1.0 Dec 22, 2021

#8 in #http-router

Download history 2/week @ 2024-02-19 6/week @ 2024-02-26 75/week @ 2024-04-01

75 downloads per month

MIT/Apache

71KB
657 lines

High performance HTTP router

Initially created as routing subsystem for Python's Squall API Framework.

Designed to mitigate the overhead of large-scale routing tables.

Squall router avoids massive Regex processing and performs validation only in those places where it is strictly necessary

The primary concept is finding relevant handlers using the HashMap-based database. Then perform computing only suitable entities and not the entire table. Regex matching is performed only for those fields which have defined data validators.

It is suitable for:

  • API Gateways
  • API services where routing cache not an option because of a lot of different parameters values
  • Just to ensure that routing is not a point of performance drop after adding a batch of new endpoints

Performance

Benchmark code based on matchit and actix-router code, so they are also here for reference.

Usage

use squall_router::SquallRouter;

fn main() {
    let mut router = SquallRouter::new();
    router.set_ignore_trailing_slashes();

    router
        .add_validator("int".to_string(), r"[0-9]+".to_string())
        .unwrap();
    router
        .add_validator(
            "uuid".to_string(),
            r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}".to_string(),
        )
        .unwrap();

    router.add_route(
        "GET".to_string(),
        "/route/without/dynamic/octets".to_string(),
        0,
    );
    router.add_route(
        "GET".to_string(),
        "/route/aaa/{string_param}/bbb/{num_param:int}/ccc/{uuid_param:uuid}".to_string(),
        1,
    );
    router.add_location("GET".to_string(), "/files/css".to_string(), 2);

    let (handler_0, _parameters_0) = router
        .resolve("GET", "/route/without/dynamic/octets")
        .unwrap();
    assert_eq!(handler_0, 0);

    let (handler_1, parameters_1) = router
        .resolve(
            "GET",
            "/route/aaa/aaa_value/bbb/1234/ccc/4bea5a51-1b80-4433-be06-d52726015591",
        )
        .unwrap();
    assert_eq!(handler_1, 1);
    assert_eq!(parameters_1, vec![
        ("string_param", "aaa_value"),
        ("num_param", "1234"),
        ("uuid_param", "4bea5a51-1b80-4433-be06-d52726015591")]
    );

    let (handler_2, _parameters_2) = router
        .resolve("GET", "/files/css/vendor/style.css")
        .unwrap();
    assert_eq!(handler_2, 2);
}

HashDoS safety note

Crate's routing database based on rustc_hash::FxHashMap which is non-cryptographic hash.

It is applicable and safe here because the database is filled only by endpoints registration and not during requests handling.

Limitations

At the moment, there are two well known limitations

Equal length routes with mixed dynamic and static parameters in the same position

/api/v1/user/{user_id}/info/full

/api/v1/user/parameter/{prm}/details

In case of such API design request to /api/v1/user/parameter/info/full will not find the handler. It is applicable only for routes with at least one dynamic parameter and with the same amount of octets. We have checked how many APIs have such behavior, it is less than 1% which can be easily aligned with this contract.

In the next releases, it will be covered by assertion to prevent a bad user experience.

There are a few ways under discussion how to make such limitations not applicable.

Wildcard route suffix

/static/{path:.*} - Dynamic routing part depends on octets splitting so it is not applicable.

Instead, you should use location api.

Dependencies

~2.1–4MB
~63K SLoC