Line data Source code
1 : use crate::auth::{AuthError, Claims, SwappableJwtAuth};
2 : use crate::http::error::{api_error_handler, route_error_handler, ApiError};
3 : use crate::http::request::{get_query_param, parse_query_param};
4 : use anyhow::{anyhow, Context};
5 : use hyper::header::{HeaderName, AUTHORIZATION, CONTENT_DISPOSITION};
6 : use hyper::http::HeaderValue;
7 : use hyper::Method;
8 : use hyper::{header::CONTENT_TYPE, Body, Request, Response};
9 : use metrics::{register_int_counter, Encoder, IntCounter, TextEncoder};
10 : use once_cell::sync::Lazy;
11 : use routerify::ext::RequestExt;
12 : use routerify::{Middleware, RequestInfo, Router, RouterBuilder};
13 : use tracing::{debug, info, info_span, warn, Instrument};
14 :
15 : use std::future::Future;
16 : use std::io::Write as _;
17 : use std::str::FromStr;
18 : use std::time::Duration;
19 :
20 : use bytes::{Bytes, BytesMut};
21 : use pprof::protos::Message as _;
22 : use tokio::sync::{mpsc, Mutex};
23 : use tokio_stream::wrappers::ReceiverStream;
24 :
25 0 : static SERVE_METRICS_COUNT: Lazy<IntCounter> = Lazy::new(|| {
26 0 : register_int_counter!(
27 0 : "libmetrics_metric_handler_requests_total",
28 0 : "Number of metric requests made"
29 0 : )
30 0 : .expect("failed to define a metric")
31 0 : });
32 :
33 : static X_REQUEST_ID_HEADER_STR: &str = "x-request-id";
34 :
35 : static X_REQUEST_ID_HEADER: HeaderName = HeaderName::from_static(X_REQUEST_ID_HEADER_STR);
36 : #[derive(Debug, Default, Clone)]
37 : struct RequestId(String);
38 :
39 : /// Adds a tracing info_span! instrumentation around the handler events,
40 : /// logs the request start and end events for non-GET requests and non-200 responses.
41 : ///
42 : /// Usage: Replace `my_handler` with `|r| request_span(r, my_handler)`
43 : ///
44 : /// Use this to distinguish between logs of different HTTP requests: every request handler wrapped
45 : /// with this will get request info logged in the wrapping span, including the unique request ID.
46 : ///
47 : /// This also handles errors, logging them and converting them to an HTTP error response.
48 : ///
49 : /// NB: If the client disconnects, Hyper will drop the Future, without polling it to
50 : /// completion. In other words, the handler must be async cancellation safe! request_span
51 : /// prints a warning to the log when that happens, so that you have some trace of it in
52 : /// the log.
53 : ///
54 : ///
55 : /// There could be other ways to implement similar functionality:
56 : ///
57 : /// * procmacros placed on top of all handler methods
58 : /// With all the drawbacks of procmacros, brings no difference implementation-wise,
59 : /// and little code reduction compared to the existing approach.
60 : ///
61 : /// * Another `TraitExt` with e.g. the `get_with_span`, `post_with_span` methods to do similar logic,
62 : /// implemented for [`RouterBuilder`].
63 : /// Could be simpler, but we don't want to depend on [`routerify`] more, targeting to use other library later.
64 : ///
65 : /// * In theory, a span guard could've been created in a pre-request middleware and placed into a global collection, to be dropped
66 : /// later, in a post-response middleware.
67 : /// Due to suspendable nature of the futures, would give contradictive results which is exactly the opposite of what `tracing-futures`
68 : /// tries to achive with its `.instrument` used in the current approach.
69 : ///
70 : /// If needed, a declarative macro to substitute the |r| ... closure boilerplate could be introduced.
71 0 : pub async fn request_span<R, H>(request: Request<Body>, handler: H) -> R::Output
72 0 : where
73 0 : R: Future<Output = Result<Response<Body>, ApiError>> + Send + 'static,
74 0 : H: FnOnce(Request<Body>) -> R + Send + Sync + 'static,
75 0 : {
76 0 : let request_id = request.context::<RequestId>().unwrap_or_default().0;
77 0 : let method = request.method();
78 0 : let path = request.uri().path();
79 0 : let request_span = info_span!("request", %method, %path, %request_id);
80 :
81 0 : let log_quietly = method == Method::GET;
82 0 : async move {
83 0 : let cancellation_guard = RequestCancelled::warn_when_dropped_without_responding();
84 0 : if log_quietly {
85 0 : debug!("Handling request");
86 : } else {
87 0 : info!("Handling request");
88 : }
89 :
90 : // No special handling for panics here. There's a `tracing_panic_hook` from another
91 : // module to do that globally.
92 0 : let res = handler(request).await;
93 :
94 0 : cancellation_guard.disarm();
95 0 :
96 0 : // Log the result if needed.
97 0 : //
98 0 : // We also convert any errors into an Ok response with HTTP error code here.
99 0 : // `make_router` sets a last-resort error handler that would do the same, but
100 0 : // we prefer to do it here, before we exit the request span, so that the error
101 0 : // is still logged with the span.
102 0 : //
103 0 : // (Because we convert errors to Ok response, we never actually return an error,
104 0 : // and we could declare the function to return the never type (`!`). However,
105 0 : // using `routerify::RouterBuilder` requires a proper error type.)
106 0 : match res {
107 0 : Ok(response) => {
108 0 : let response_status = response.status();
109 0 : if log_quietly && response_status.is_success() {
110 0 : debug!("Request handled, status: {response_status}");
111 : } else {
112 0 : info!("Request handled, status: {response_status}");
113 : }
114 0 : Ok(response)
115 : }
116 0 : Err(err) => Ok(api_error_handler(err)),
117 : }
118 0 : }
119 0 : .instrument(request_span)
120 0 : .await
121 0 : }
122 :
123 : /// Drop guard to WARN in case the request was dropped before completion.
124 : struct RequestCancelled {
125 : warn: Option<tracing::Span>,
126 : }
127 :
128 : impl RequestCancelled {
129 : /// Create the drop guard using the [`tracing::Span::current`] as the span.
130 0 : fn warn_when_dropped_without_responding() -> Self {
131 0 : RequestCancelled {
132 0 : warn: Some(tracing::Span::current()),
133 0 : }
134 0 : }
135 :
136 : /// Consume the drop guard without logging anything.
137 0 : fn disarm(mut self) {
138 0 : self.warn = None;
139 0 : }
140 : }
141 :
142 : impl Drop for RequestCancelled {
143 0 : fn drop(&mut self) {
144 0 : if std::thread::panicking() {
145 0 : // we are unwinding due to panicking, assume we are not dropped for cancellation
146 0 : } else if let Some(span) = self.warn.take() {
147 : // the span has all of the info already, but the outer `.instrument(span)` has already
148 : // been dropped, so we need to manually re-enter it for this message.
149 : //
150 : // this is what the instrument would do before polling so it is fine.
151 0 : let _g = span.entered();
152 0 : warn!("request was dropped before completing");
153 0 : }
154 0 : }
155 : }
156 :
157 : /// An [`std::io::Write`] implementation on top of a channel sending [`bytes::Bytes`] chunks.
158 : pub struct ChannelWriter {
159 : buffer: BytesMut,
160 : pub tx: mpsc::Sender<std::io::Result<Bytes>>,
161 : written: usize,
162 : /// Time spent waiting for the channel to make progress. It is not the same as time to upload a
163 : /// buffer because we cannot know anything about that, but this should allow us to understand
164 : /// the actual time taken without the time spent `std::thread::park`ed.
165 : wait_time: std::time::Duration,
166 : }
167 :
168 : impl ChannelWriter {
169 0 : pub fn new(buf_len: usize, tx: mpsc::Sender<std::io::Result<Bytes>>) -> Self {
170 0 : assert_ne!(buf_len, 0);
171 0 : ChannelWriter {
172 0 : // split about half off the buffer from the start, because we flush depending on
173 0 : // capacity. first flush will come sooner than without this, but now resizes will
174 0 : // have better chance of picking up the "other" half. not guaranteed of course.
175 0 : buffer: BytesMut::with_capacity(buf_len).split_off(buf_len / 2),
176 0 : tx,
177 0 : written: 0,
178 0 : wait_time: std::time::Duration::ZERO,
179 0 : }
180 0 : }
181 :
182 0 : pub fn flush0(&mut self) -> std::io::Result<usize> {
183 0 : let n = self.buffer.len();
184 0 : if n == 0 {
185 0 : return Ok(0);
186 0 : }
187 0 :
188 0 : tracing::trace!(n, "flushing");
189 0 : let ready = self.buffer.split().freeze();
190 0 :
191 0 : let wait_started_at = std::time::Instant::now();
192 0 :
193 0 : // not ideal to call from blocking code to block_on, but we are sure that this
194 0 : // operation does not spawn_blocking other tasks
195 0 : let res: Result<(), ()> = tokio::runtime::Handle::current().block_on(async {
196 0 : self.tx.send(Ok(ready)).await.map_err(|_| ())?;
197 :
198 : // throttle sending to allow reuse of our buffer in `write`.
199 0 : self.tx.reserve().await.map_err(|_| ())?;
200 :
201 : // now the response task has picked up the buffer and hopefully started
202 : // sending it to the client.
203 0 : Ok(())
204 0 : });
205 0 :
206 0 : self.wait_time += wait_started_at.elapsed();
207 0 :
208 0 : if res.is_err() {
209 0 : return Err(std::io::ErrorKind::BrokenPipe.into());
210 0 : }
211 0 : self.written += n;
212 0 : Ok(n)
213 0 : }
214 :
215 0 : pub fn flushed_bytes(&self) -> usize {
216 0 : self.written
217 0 : }
218 :
219 0 : pub fn wait_time(&self) -> std::time::Duration {
220 0 : self.wait_time
221 0 : }
222 : }
223 :
224 : impl std::io::Write for ChannelWriter {
225 0 : fn write(&mut self, mut buf: &[u8]) -> std::io::Result<usize> {
226 0 : let remaining = self.buffer.capacity() - self.buffer.len();
227 0 :
228 0 : let out_of_space = remaining < buf.len();
229 0 :
230 0 : let original_len = buf.len();
231 0 :
232 0 : if out_of_space {
233 0 : let can_still_fit = buf.len() - remaining;
234 0 : self.buffer.extend_from_slice(&buf[..can_still_fit]);
235 0 : buf = &buf[can_still_fit..];
236 0 : self.flush0()?;
237 0 : }
238 :
239 : // assume that this will often under normal operation just move the pointer back to the
240 : // beginning of allocation, because previous split off parts are already sent and
241 : // dropped.
242 0 : self.buffer.extend_from_slice(buf);
243 0 : Ok(original_len)
244 0 : }
245 :
246 0 : fn flush(&mut self) -> std::io::Result<()> {
247 0 : self.flush0().map(|_| ())
248 0 : }
249 : }
250 :
251 0 : pub async fn prometheus_metrics_handler(_req: Request<Body>) -> Result<Response<Body>, ApiError> {
252 0 : SERVE_METRICS_COUNT.inc();
253 0 :
254 0 : let started_at = std::time::Instant::now();
255 0 :
256 0 : let (tx, rx) = mpsc::channel(1);
257 0 :
258 0 : let body = Body::wrap_stream(ReceiverStream::new(rx));
259 0 :
260 0 : let mut writer = ChannelWriter::new(128 * 1024, tx);
261 0 :
262 0 : let encoder = TextEncoder::new();
263 0 :
264 0 : let response = Response::builder()
265 0 : .status(200)
266 0 : .header(CONTENT_TYPE, encoder.format_type())
267 0 : .body(body)
268 0 : .unwrap();
269 :
270 0 : let span = info_span!("blocking");
271 0 : tokio::task::spawn_blocking(move || {
272 0 : // there are situations where we lose scraped metrics under load, try to gather some clues
273 0 : // since all nodes are queried this, keep the message count low.
274 0 : let spawned_at = std::time::Instant::now();
275 0 :
276 0 : let _span = span.entered();
277 0 :
278 0 : let metrics = metrics::gather();
279 0 :
280 0 : let gathered_at = std::time::Instant::now();
281 0 :
282 0 : let res = encoder
283 0 : .encode(&metrics, &mut writer)
284 0 : .and_then(|_| writer.flush().map_err(|e| e.into()));
285 0 :
286 0 : // this instant is not when we finally got the full response sent, sending is done by hyper
287 0 : // in another task.
288 0 : let encoded_at = std::time::Instant::now();
289 0 :
290 0 : let spawned_in = spawned_at - started_at;
291 0 : let collected_in = gathered_at - spawned_at;
292 0 : // remove the wait time here in case the tcp connection was clogged
293 0 : let encoded_in = encoded_at - gathered_at - writer.wait_time();
294 0 : let total = encoded_at - started_at;
295 0 :
296 0 : match res {
297 : Ok(()) => {
298 0 : tracing::info!(
299 0 : bytes = writer.flushed_bytes(),
300 0 : total_ms = total.as_millis(),
301 0 : spawning_ms = spawned_in.as_millis(),
302 0 : collection_ms = collected_in.as_millis(),
303 0 : encoding_ms = encoded_in.as_millis(),
304 0 : "responded /metrics"
305 : );
306 : }
307 0 : Err(e) => {
308 0 : // there is a chance that this error is not the BrokenPipe we generate in the writer
309 0 : // for "closed connection", but it is highly unlikely.
310 0 : tracing::warn!(
311 0 : after_bytes = writer.flushed_bytes(),
312 0 : total_ms = total.as_millis(),
313 0 : spawning_ms = spawned_in.as_millis(),
314 0 : collection_ms = collected_in.as_millis(),
315 0 : encoding_ms = encoded_in.as_millis(),
316 0 : "failed to write out /metrics response: {e:?}"
317 : );
318 : // semantics of this error are quite... unclear. we want to error the stream out to
319 : // abort the response to somehow notify the client that we failed.
320 : //
321 : // though, most likely the reason for failure is that the receiver is already gone.
322 0 : drop(
323 0 : writer
324 0 : .tx
325 0 : .blocking_send(Err(std::io::ErrorKind::BrokenPipe.into())),
326 0 : );
327 : }
328 : }
329 0 : });
330 0 :
331 0 : Ok(response)
332 0 : }
333 :
334 : /// Generates CPU profiles.
335 0 : pub async fn profile_cpu_handler(req: Request<Body>) -> Result<Response<Body>, ApiError> {
336 : enum Format {
337 : Pprof,
338 : Svg,
339 : }
340 :
341 : // Parameters.
342 0 : let format = match get_query_param(&req, "format")?.as_deref() {
343 0 : None => Format::Pprof,
344 0 : Some("pprof") => Format::Pprof,
345 0 : Some("svg") => Format::Svg,
346 0 : Some(format) => return Err(ApiError::BadRequest(anyhow!("invalid format {format}"))),
347 : };
348 0 : let seconds = match parse_query_param(&req, "seconds")? {
349 0 : None => 5,
350 0 : Some(seconds @ 1..=30) => seconds,
351 0 : Some(_) => return Err(ApiError::BadRequest(anyhow!("duration must be 1-30 secs"))),
352 : };
353 0 : let frequency_hz = match parse_query_param(&req, "frequency")? {
354 0 : None => 99,
355 0 : Some(1001..) => return Err(ApiError::BadRequest(anyhow!("frequency must be <=1000 Hz"))),
356 0 : Some(frequency) => frequency,
357 : };
358 :
359 : // Only allow one profiler at a time.
360 0 : static PROFILE_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
361 0 : let _lock = PROFILE_LOCK
362 0 : .try_lock()
363 0 : .map_err(|_| ApiError::Conflict("profiler already running".into()))?;
364 :
365 : // Take the profile.
366 0 : let report = tokio::task::spawn_blocking(move || {
367 0 : let guard = pprof::ProfilerGuardBuilder::default()
368 0 : .frequency(frequency_hz)
369 0 : .blocklist(&["libc", "libgcc", "pthread", "vdso"])
370 0 : .build()?;
371 0 : std::thread::sleep(Duration::from_secs(seconds));
372 0 : guard.report().build()
373 0 : })
374 0 : .await
375 0 : .map_err(|join_err| ApiError::InternalServerError(join_err.into()))?
376 0 : .map_err(|pprof_err| ApiError::InternalServerError(pprof_err.into()))?;
377 :
378 : // Return the report in the requested format.
379 0 : match format {
380 : Format::Pprof => {
381 0 : let mut body = Vec::new();
382 0 : report
383 0 : .pprof()
384 0 : .map_err(|err| ApiError::InternalServerError(err.into()))?
385 0 : .write_to_vec(&mut body)
386 0 : .map_err(|err| ApiError::InternalServerError(err.into()))?;
387 :
388 0 : Response::builder()
389 0 : .status(200)
390 0 : .header(CONTENT_TYPE, "application/octet-stream")
391 0 : .header(CONTENT_DISPOSITION, "attachment; filename=\"profile.pb\"")
392 0 : .body(Body::from(body))
393 0 : .map_err(|err| ApiError::InternalServerError(err.into()))
394 : }
395 :
396 : Format::Svg => {
397 0 : let mut body = Vec::new();
398 0 : report
399 0 : .flamegraph(&mut body)
400 0 : .map_err(|err| ApiError::InternalServerError(err.into()))?;
401 0 : Response::builder()
402 0 : .status(200)
403 0 : .header(CONTENT_TYPE, "image/svg+xml")
404 0 : .body(Body::from(body))
405 0 : .map_err(|err| ApiError::InternalServerError(err.into()))
406 : }
407 : }
408 0 : }
409 :
410 2 : pub fn add_request_id_middleware<B: hyper::body::HttpBody + Send + Sync + 'static>(
411 2 : ) -> Middleware<B, ApiError> {
412 2 : Middleware::pre(move |req| async move {
413 2 : let request_id = match req.headers().get(&X_REQUEST_ID_HEADER) {
414 1 : Some(request_id) => request_id
415 1 : .to_str()
416 1 : .expect("extract request id value")
417 1 : .to_owned(),
418 : None => {
419 1 : let request_id = uuid::Uuid::new_v4();
420 1 : request_id.to_string()
421 : }
422 : };
423 2 : req.set_context(RequestId(request_id));
424 2 :
425 2 : Ok(req)
426 2 : })
427 2 : }
428 :
429 2 : async fn add_request_id_header_to_response(
430 2 : mut res: Response<Body>,
431 2 : req_info: RequestInfo,
432 2 : ) -> Result<Response<Body>, ApiError> {
433 2 : if let Some(request_id) = req_info.context::<RequestId>() {
434 2 : if let Ok(request_header_value) = HeaderValue::from_str(&request_id.0) {
435 2 : res.headers_mut()
436 2 : .insert(&X_REQUEST_ID_HEADER, request_header_value);
437 2 : };
438 0 : };
439 :
440 2 : Ok(res)
441 2 : }
442 :
443 2 : pub fn make_router() -> RouterBuilder<hyper::Body, ApiError> {
444 2 : Router::builder()
445 2 : .middleware(add_request_id_middleware())
446 2 : .middleware(Middleware::post_with_info(
447 2 : add_request_id_header_to_response,
448 2 : ))
449 2 : .err_handler(route_error_handler)
450 2 : }
451 :
452 0 : pub fn attach_openapi_ui(
453 0 : router_builder: RouterBuilder<hyper::Body, ApiError>,
454 0 : spec: &'static [u8],
455 0 : spec_mount_path: &'static str,
456 0 : ui_mount_path: &'static str,
457 0 : ) -> RouterBuilder<hyper::Body, ApiError> {
458 0 : router_builder
459 0 : .get(spec_mount_path,
460 0 : move |r| request_span(r, move |_| async move {
461 0 : Ok(Response::builder().body(Body::from(spec)).unwrap())
462 0 : })
463 0 : )
464 0 : .get(ui_mount_path,
465 0 : move |r| request_span(r, move |_| async move {
466 0 : Ok(Response::builder().body(Body::from(format!(r#"
467 0 : <!DOCTYPE html>
468 0 : <html lang="en">
469 0 : <head>
470 0 : <title>rweb</title>
471 0 : <link href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui.css" rel="stylesheet">
472 0 : </head>
473 0 : <body>
474 0 : <div id="swagger-ui"></div>
475 0 : <script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@3/swagger-ui-bundle.js" charset="UTF-8"> </script>
476 0 : <script>
477 0 : window.onload = function() {{
478 0 : const ui = SwaggerUIBundle({{
479 0 : "dom_id": "\#swagger-ui",
480 0 : presets: [
481 0 : SwaggerUIBundle.presets.apis,
482 0 : SwaggerUIBundle.SwaggerUIStandalonePreset
483 0 : ],
484 0 : layout: "BaseLayout",
485 0 : deepLinking: true,
486 0 : showExtensions: true,
487 0 : showCommonExtensions: true,
488 0 : url: "{}",
489 0 : }})
490 0 : window.ui = ui;
491 0 : }};
492 0 : </script>
493 0 : </body>
494 0 : </html>
495 0 : "#, spec_mount_path))).unwrap())
496 0 : })
497 0 : )
498 0 : }
499 :
500 0 : fn parse_token(header_value: &str) -> Result<&str, ApiError> {
501 : // header must be in form Bearer <token>
502 0 : let (prefix, token) = header_value
503 0 : .split_once(' ')
504 0 : .ok_or_else(|| ApiError::Unauthorized("malformed authorization header".to_string()))?;
505 0 : if prefix != "Bearer" {
506 0 : return Err(ApiError::Unauthorized(
507 0 : "malformed authorization header".to_string(),
508 0 : ));
509 0 : }
510 0 : Ok(token)
511 0 : }
512 :
513 0 : pub fn auth_middleware<B: hyper::body::HttpBody + Send + Sync + 'static>(
514 0 : provide_auth: fn(&Request<Body>) -> Option<&SwappableJwtAuth>,
515 0 : ) -> Middleware<B, ApiError> {
516 0 : Middleware::pre(move |req| async move {
517 0 : if let Some(auth) = provide_auth(&req) {
518 0 : match req.headers().get(AUTHORIZATION) {
519 0 : Some(value) => {
520 0 : let header_value = value.to_str().map_err(|_| {
521 0 : ApiError::Unauthorized("malformed authorization header".to_string())
522 0 : })?;
523 0 : let token = parse_token(header_value)?;
524 :
525 0 : let data = auth.decode(token).map_err(|err| {
526 0 : warn!("Authentication error: {err}");
527 : // Rely on From<AuthError> for ApiError impl
528 0 : err
529 0 : })?;
530 0 : req.set_context(data.claims);
531 : }
532 : None => {
533 0 : return Err(ApiError::Unauthorized(
534 0 : "missing authorization header".to_string(),
535 0 : ))
536 : }
537 : }
538 0 : }
539 0 : Ok(req)
540 0 : })
541 0 : }
542 :
543 0 : pub fn add_response_header_middleware<B>(
544 0 : header: &str,
545 0 : value: &str,
546 0 : ) -> anyhow::Result<Middleware<B, ApiError>>
547 0 : where
548 0 : B: hyper::body::HttpBody + Send + Sync + 'static,
549 0 : {
550 0 : let name =
551 0 : HeaderName::from_str(header).with_context(|| format!("invalid header name: {header}"))?;
552 0 : let value =
553 0 : HeaderValue::from_str(value).with_context(|| format!("invalid header value: {value}"))?;
554 0 : Ok(Middleware::post_with_info(
555 0 : move |mut response, request_info| {
556 0 : let name = name.clone();
557 0 : let value = value.clone();
558 0 : async move {
559 0 : let headers = response.headers_mut();
560 0 : if headers.contains_key(&name) {
561 0 : warn!(
562 0 : "{} response already contains header {:?}",
563 0 : request_info.uri(),
564 0 : &name,
565 : );
566 0 : } else {
567 0 : headers.insert(name, value);
568 0 : }
569 0 : Ok(response)
570 0 : }
571 0 : },
572 0 : ))
573 0 : }
574 :
575 0 : pub fn check_permission_with(
576 0 : req: &Request<Body>,
577 0 : check_permission: impl Fn(&Claims) -> Result<(), AuthError>,
578 0 : ) -> Result<(), ApiError> {
579 0 : match req.context::<Claims>() {
580 0 : Some(claims) => Ok(check_permission(&claims)
581 0 : .map_err(|_err| ApiError::Forbidden("JWT authentication error".to_string()))?),
582 0 : None => Ok(()), // claims is None because auth is disabled
583 : }
584 0 : }
585 :
586 : #[cfg(test)]
587 : mod tests {
588 : use super::*;
589 : use futures::future::poll_fn;
590 : use hyper::service::Service;
591 : use routerify::RequestServiceBuilder;
592 : use std::net::{IpAddr, SocketAddr};
593 :
594 : #[tokio::test]
595 1 : async fn test_request_id_returned() {
596 1 : let builder = RequestServiceBuilder::new(make_router().build().unwrap()).unwrap();
597 1 : let remote_addr = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), 80);
598 1 : let mut service = builder.build(remote_addr);
599 1 : if let Err(e) = poll_fn(|ctx| service.poll_ready(ctx)).await {
600 1 : panic!("request service is not ready: {:?}", e);
601 1 : }
602 1 :
603 1 : let mut req: Request<Body> = Request::default();
604 1 : req.headers_mut()
605 1 : .append(&X_REQUEST_ID_HEADER, HeaderValue::from_str("42").unwrap());
606 1 :
607 1 : let resp: Response<hyper::body::Body> = service.call(req).await.unwrap();
608 1 :
609 1 : let header_val = resp.headers().get(&X_REQUEST_ID_HEADER).unwrap();
610 1 :
611 1 : assert!(header_val == "42", "response header mismatch");
612 1 : }
613 :
614 : #[tokio::test]
615 1 : async fn test_request_id_empty() {
616 1 : let builder = RequestServiceBuilder::new(make_router().build().unwrap()).unwrap();
617 1 : let remote_addr = SocketAddr::new(IpAddr::from_str("127.0.0.1").unwrap(), 80);
618 1 : let mut service = builder.build(remote_addr);
619 1 : if let Err(e) = poll_fn(|ctx| service.poll_ready(ctx)).await {
620 1 : panic!("request service is not ready: {:?}", e);
621 1 : }
622 1 :
623 1 : let req: Request<Body> = Request::default();
624 1 : let resp: Response<hyper::body::Body> = service.call(req).await.unwrap();
625 1 :
626 1 : let header_val = resp.headers().get(&X_REQUEST_ID_HEADER);
627 1 :
628 1 : assert_ne!(header_val, None, "response header should NOT be empty");
629 1 : }
630 : }
|