http_types/trace/server_timing/
parse.rs

1use std::time::Duration;
2
3use super::Metric;
4use crate::{ensure, format_err, StatusCode};
5
6/// Parse multiple entries from a single header.
7///
8/// Each entry is comma-delimited.
9pub(super) fn parse_header(s: &str, entries: &mut Vec<Metric>) -> crate::Result<()> {
10    for part in s.trim().split(',') {
11        let entry = parse_entry(part).map_err(|mut e| {
12            e.set_status(StatusCode::BadRequest);
13            e
14        })?;
15        entries.push(entry);
16    }
17    Ok(())
18}
19
20/// Create an entry from a string. Parsing rules in ABNF are:
21//
22/// ```txt
23/// Server-Timing             = #server-timing-metric
24/// server-timing-metric      = metric-name *( OWS ";" OWS server-timing-param )
25/// metric-name               = token
26/// server-timing-param       = server-timing-param-name OWS "=" OWS server-timing-param-value
27/// server-timing-param-name  = token
28/// server-timing-param-value = token / quoted-string
29/// ```
30//
31/// Source: https://w3c.github.io/server-timing/#the-server-timing-header-field
32fn parse_entry(s: &str) -> crate::Result<Metric> {
33    let mut parts = s.trim().split(';');
34
35    // Get the name. This is non-optional.
36    let name = parts
37        .next()
38        .ok_or_else(|| format_err!("Server timing headers must include a name"))?
39        .trim_end();
40
41    // We must extract these values from the k-v pairs that follow.
42    let mut dur = None;
43    let mut desc = None;
44
45    for mut part in parts {
46        ensure!(
47            !part.is_empty(),
48            "Server timing params cannot end with a trailing `;`"
49        );
50
51        part = part.trim_start();
52
53        let mut params = part.split('=');
54        let name = params
55            .next()
56            .ok_or_else(|| format_err!("Server timing params must have a name"))?
57            .trim_end();
58        let mut value = params
59            .next()
60            .ok_or_else(|| format_err!("Server timing params must have a value"))?
61            .trim_start();
62
63        match name {
64            "dur" => {
65                let millis: f64 = value.parse().map_err(|_| {
66                        format_err!("Server timing duration params must be a valid double-precision floating-point number.")
67                    })?;
68                dur = Some(Duration::from_secs_f64(millis / 1000.0));
69            }
70            "desc" => {
71                // Ensure quotes line up, and strip them from the resulting output
72                if value.starts_with('"') {
73                    value = &value[1..value.len()];
74                    ensure!(
75                        value.ends_with('"'),
76                        "Server timing description params must use matching quotes"
77                    );
78                    value = &value[0..value.len() - 1];
79                } else {
80                    ensure!(
81                        !value.ends_with('"'),
82                        "Server timing description params must use matching quotes"
83                    );
84                }
85                desc = Some(value.to_string());
86            }
87            _ => continue,
88        }
89    }
90
91    Ok(Metric {
92        name: name.to_string(),
93        dur,
94        desc,
95    })
96}
97
98#[cfg(test)]
99mod test {
100    use super::*;
101
102    #[test]
103    fn decode_header() -> crate::Result<()> {
104        // Metric name only.
105        assert_entry("Server", "Server", None, None)?;
106        assert_entry("Server ", "Server", None, None)?;
107        assert_entry_err(
108            "Server ;",
109            "Server timing params cannot end with a trailing `;`",
110        );
111        assert_entry_err(
112            "Server; ",
113            "Server timing params cannot end with a trailing `;`",
114        );
115
116        // Metric name + param
117        assert_entry("Server; dur=1000", "Server", Some(1000), None)?;
118        assert_entry("Server; dur =1000", "Server", Some(1000), None)?;
119        assert_entry("Server; dur= 1000", "Server", Some(1000), None)?;
120        assert_entry("Server; dur = 1000", "Server", Some(1000), None)?;
121        assert_entry_err(
122            "Server; dur=1000;",
123            "Server timing params cannot end with a trailing `;`",
124        );
125
126        // Metric name + desc
127        assert_entry(r#"DB; desc="a db""#, "DB", None, Some("a db"))?;
128        assert_entry(r#"DB; desc ="a db""#, "DB", None, Some("a db"))?;
129        assert_entry(r#"DB; desc= "a db""#, "DB", None, Some("a db"))?;
130        assert_entry(r#"DB; desc = "a db""#, "DB", None, Some("a db"))?;
131        assert_entry(r#"DB; desc=a_db"#, "DB", None, Some("a_db"))?;
132        assert_entry_err(
133            r#"DB; desc="db"#,
134            "Server timing description params must use matching quotes",
135        );
136        assert_entry_err(
137            "Server; desc=a_db;",
138            "Server timing params cannot end with a trailing `;`",
139        );
140
141        // Metric name + dur + desc
142        assert_entry(
143            r#"Server; dur=1000; desc="a server""#,
144            "Server",
145            Some(1000),
146            Some("a server"),
147        )?;
148        assert_entry_err(
149            r#"Server; dur=1000; desc="a server";"#,
150            "Server timing params cannot end with a trailing `;`",
151        );
152        Ok(())
153    }
154
155    #[test]
156    fn decode_headers() -> crate::Result<()> {
157        // Example from MDN.
158        let mut entries = vec![];
159        parse_header("db;dur=53, app;dur=47.2", &mut entries)?;
160        let e = &entries[0];
161        assert_eq!(e.name(), "db");
162        assert_eq!(e.duration(), Some(Duration::from_millis(53)));
163        let e = &entries[1];
164        assert_eq!(e.name(), "app");
165        assert_eq!(e.duration(), Some(Duration::from_micros(47200)));
166        Ok(())
167    }
168
169    fn assert_entry_err(s: &str, msg: &str) {
170        let err = parse_entry(s).unwrap_err();
171        assert_eq!(format!("{}", err), msg);
172    }
173
174    /// Assert an entry and all of its fields.
175    fn assert_entry(s: &str, n: &str, du: Option<u64>, de: Option<&str>) -> crate::Result<()> {
176        let e = parse_entry(s)?;
177        assert_eq!(e.name(), n);
178        assert_eq!(e.duration(), du.map(Duration::from_millis));
179        assert_eq!(e.description(), de);
180        Ok(())
181    }
182}