Skip to content

Commit cf6efc6

Browse files
committed
feat(server): add API backwards compatibility
Some examples of how this works: ``` $ curl --cacert testdata/ca.crt https://localhost:8080/api/v0.0.0/test Bearer token header missing $ curl --cacert testdata/ca.crt https://localhost:8080/api/v0.0.1/test Bearer token header missing $ curl --cacert testdata/ca.crt https://localhost:8080/api/v1.0.1/test Unsupported API version `1.0.1` $ curl --cacert testdata/ca.crt https://localhost:8080/api/v0.1.0/test Bearer token header missing $ curl --cacert testdata/ca.crt https://localhost:8080/api/v0.1.0-rc3/test Bearer token header missing $ curl --cacert testdata/ca.crt https://localhost:8080/api/v0.2.0-rc5/test Bearer token header missing $ curl --cacert testdata/ca.crt https://localhost:8080/api/v0.2.0/test Bearer token header missing $ curl --cacert testdata/ca.crt https://localhost:8080/api/v0.2.1-rc1/test Unsupported API version `0.2.1-rc1` $ curl --cacert testdata/ca.crt https://localhost:8080/api/v0.2.1/test Unsupported API version `0.2.1` $ curl --cacert testdata/ca.crt https://localhost:8080/api/v1.0.0/test Unsupported API version `1.0.0` ``` Signed-off-by: Roman Volosatovs <[email protected]>
1 parent 4d3d3e3 commit cf6efc6

File tree

3 files changed

+36
-9
lines changed

3 files changed

+36
-9
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/server/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ cap-async-std = { version = "0.24.4", default-features = true, features = ["fs_u
2323
futures = { version = "0.3.21", default-features = false, features = ["async-await"] }
2424
futures-rustls = { version = "0.22.1", default-features = false }
2525
hyper = { version = "0.14.18", default-features = false, features = ["http1", "server"] }
26+
lazy_static = { version = "1.4.0", default-features = false }
2627
log = { version = "0.4.17", default-features = false, features = ["release_max_level_debug"] }
2728
mime = { version = "0.3.16", default-features = false }
2829
openidconnect = { version = "2.3.1", default-features = false, features = ["ureq"] }
2930
rustls = { version = "0.20.6", default-features = false }
3031
rustls-pemfile = { version = "1.0.0", default-features = false }
32+
semver = { version = "1.0.12", default-features = false }
3133
serde = { version = "1.0.136", default-features = false }
3234
serde_json = { version = "1.0.79", default-features = false, features = ["std"] }
3335
tokio-util = { version = "0.7.3", default-features = false, features = ["compat"] }

crates/server/src/handle.rs

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,53 @@ use axum::body::Body;
99
use axum::handler::Handler;
1010
use axum::http::{Method, Request, StatusCode};
1111
use axum::response::IntoResponse;
12+
use lazy_static::lazy_static;
1213
use log::trace;
1314
use tower::Service;
1415

16+
lazy_static! {
17+
static ref API_VERSION: semver::Version = env!("CARGO_PKG_VERSION").parse().expect(&format!(
18+
"failed to parse CARGO_PKG_VERSION `{}`",
19+
env!("CARGO_PKG_VERSION")
20+
));
21+
}
22+
1523
/// Parses the URI of `req` and routes it to respective component.
1624
pub async fn handle(mut req: Request<Body>) -> impl IntoResponse {
25+
#[inline]
26+
fn not_found(path: &str) -> (StatusCode, String) {
27+
(StatusCode::NOT_FOUND, format!("Route `/{path}` not found"))
28+
}
29+
1730
trace!(target: "app::handle", "begin HTTP request handling {:?}", req);
1831
let path = req.uri().path().trim_start_matches('/');
19-
let path = path
32+
let (ver, path) = path
2033
.strip_prefix("api")
21-
.ok_or((StatusCode::NOT_FOUND, format!("Route `/{path}` not found")))?
34+
.ok_or_else(|| not_found(path))?
2235
.trim_start_matches('/')
23-
// TODO: Parse SemVer, support v0, v0.1 etc.
24-
.strip_prefix("v0.1.0")
25-
.ok_or((
36+
.strip_prefix('v')
37+
.ok_or_else(|| not_found(path))?
38+
.split_once('/')
39+
.ok_or_else(|| not_found(path))?;
40+
let ver = ver.parse::<semver::Version>().map_err(|e| {
41+
(
42+
StatusCode::BAD_REQUEST,
43+
format!("Failed to parse SemVer version from {path}: {e}"),
44+
)
45+
})?;
46+
if ver > *API_VERSION {
47+
return Err((
2648
StatusCode::NOT_IMPLEMENTED,
27-
"Unsupported API version".into(),
28-
))?
29-
.trim_start_matches('/');
49+
format!("Unsupported API version `{ver}`"),
50+
));
51+
}
3052
let (head, tail) = path
53+
.trim_start_matches('/')
3154
.split_once("/_")
3255
.map(|(left, right)| (left.to_string(), format!("_{right}")))
3356
.unwrap_or((path.to_string(), "".into()));
3457
if head.is_empty() {
35-
return Err((StatusCode::NOT_FOUND, format!("Route `/{path}` not found")));
58+
return Err(not_found(path));
3659
}
3760

3861
let extensions = req.extensions_mut();

0 commit comments

Comments
 (0)