[Security][Rust Frontend] Normalize HTTP method labels in metrics (#56058)
Co-authored-by: Bugen Zhao <i@bugenzhao.com> Co-authored-by: OpenAI Codex <noreply@openai.com> Signed-off-by: Juan Pérez de Algaba <jperezde@redhat.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Juan Pérez de Algaba committed
Sep 10, 2026 at 04:54 UTC
3735c2d5f5248259482b9045c34fb7a8a3892352
2 files changed
+109
-1
rust/src/server/src/middleware/metrics.rs
+51
-1
@@ -4,6 +4,7 @@
4
use std::time::Instant;
5
6
use axum::extract::{MatchedPath, Request};
7
+use axum::http::Method;
8
use axum::middleware::Next;
9
use axum::response::Response;
10
use vllm_metrics::{HttpHandlerLabels, HttpRequestLabels, METRICS};
@@ -29,10 +30,29 @@ const EXCLUDED_HANDLERS: &[&str] = &[
30
"/is_sleeping",
31
];
32
33
+/// Map the request method to a bounded Prometheus label.
34
+///
35
+/// HTTP parsers accept arbitrary method tokens. Recording the raw token as a
36
+/// label would let unique values create unbounded time series.
37
+pub(crate) fn http_metrics_method(method: &Method) -> &str {
38
+ match *method {
39
+ Method::GET
40
+ | Method::POST
41
+ | Method::PUT
42
+ | Method::PATCH
43
+ | Method::DELETE
44
+ | Method::HEAD
45
+ | Method::OPTIONS
46
+ | Method::CONNECT
47
+ | Method::TRACE => method.as_str(),
48
+ _ => "other",
49
+ }
50
+}
51
+
52
/// Record API-server HTTP metrics with Python-compatible
53
/// (`PrometheusFastApiInstrumentator` style) family names and labels.
54
pub async fn track_http_metrics(req: Request, next: Next) -> Response {
35
- let method = req.method().as_str().to_string();
55
+ let method = http_metrics_method(req.method()).to_string();
56
let handler = req
57
.extensions()
58
.get::<MatchedPath>()
@@ -80,3 +100,33 @@ fn status_group(status: u16) -> &'static str {
100
_ => "unknown",
101
}
102
}
103
+
104
+#[cfg(test)]
105
+mod tests {
106
+ use super::*;
107
+
108
+ #[test]
109
+ fn known_methods_keep_their_names() {
110
+ for (method, expected) in [
111
+ (Method::GET, "GET"),
112
+ (Method::POST, "POST"),
113
+ (Method::PUT, "PUT"),
114
+ (Method::PATCH, "PATCH"),
115
+ (Method::DELETE, "DELETE"),
116
+ (Method::HEAD, "HEAD"),
117
+ (Method::OPTIONS, "OPTIONS"),
118
+ (Method::CONNECT, "CONNECT"),
119
+ (Method::TRACE, "TRACE"),
120
+ ] {
121
+ assert_eq!(http_metrics_method(&method), expected);
122
+ }
123
+ }
124
+
125
+ #[test]
126
+ fn unknown_methods_collapse_to_other() {
127
+ let method = Method::from_bytes(b"XVULNCARD000001").expect("valid token");
128
+ assert_eq!(http_metrics_method(&method), "other");
129
+ let other = Method::from_bytes(b"XVULNCARD000002").expect("valid token");
130
+ assert_eq!(http_metrics_method(&other), "other");
131
+ }
132
+}
rust/src/server/src/routes/tests.rs
+58
@@ -3115,6 +3115,64 @@ async fn http_metrics_group_error_statuses() {
3115
);
3116
}
3117
3118
+#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3119
+#[serial]
3120
+async fn http_metrics_collapse_unknown_methods() {
3121
+ let mut app = test_app().await;
3122
+ let before = METRICS.render().unwrap();
3123
+
3124
+ for (method, path) in [
3125
+ ("XVULNCARD000001", "/tokenize"),
3126
+ ("XVULNCARD000002", "/tokenize"),
3127
+ ("XVULNCARD000003", "/detokenize"),
3128
+ ] {
3129
+ let response = app
3130
+ .call(
3131
+ Request::builder()
3132
+ .method(method)
3133
+ .uri(path)
3134
+ .body(Body::empty())
3135
+ .expect("build request"),
3136
+ )
3137
+ .await
3138
+ .expect("call app");
3139
+ assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED);
3140
+ }
3141
+
3142
+ let after = METRICS.render().unwrap();
3143
+ assert!(
3144
+ !after.contains("XVULNCARD"),
3145
+ "raw method tokens must not appear in metrics: {after}"
3146
+ );
3147
+ assert_eq!(
3148
+ metric_delta(
3149
+ &before,
3150
+ &after,
3151
+ "http_requests_total",
3152
+ Some("method=\"other\",status=\"4xx\",handler=\"/tokenize\""),
3153
+ ),
3154
+ 2.0
3155
+ );
3156
+ assert_eq!(
3157
+ metric_delta(
3158
+ &before,
3159
+ &after,
3160
+ "http_requests_total",
3161
+ Some("method=\"other\",status=\"4xx\",handler=\"/detokenize\""),
3162
+ ),
3163
+ 1.0
3164
+ );
3165
+ assert_eq!(
3166
+ metric_delta(
3167
+ &before,
3168
+ &after,
3169
+ "http_request_duration_seconds_count",
3170
+ Some("method=\"other\",handler=\"/tokenize\""),
3171
+ ),
3172
+ 2.0
3173
+ );
3174
+}
3175
+
3176
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
3177
#[serial]
3178
async fn load_endpoint_tracks_chat_stream_lifecycle() {