-
Notifications
You must be signed in to change notification settings - Fork 196
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add request ID to response headers #2438
Merged
Merged
Changes from 19 commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
fdf1f2d
Add request ID to response headers
82marbag e6dcb1d
Add parsing test
82marbag 29ea8fe
Style
82marbag 61a1a15
CHANGELOG
82marbag 0c8f3a1
Fix import
82marbag d12cbb1
Panic if ServerRequestIdProviderLayer is not present
82marbag 265be65
Own value
82marbag 0b98fd8
Correct docs
82marbag f75de9e
Add order of layer to expect() message
82marbag c24b86b
Remove Box
82marbag ae9658d
Require order of request ID layers
82marbag 64c2acd
Revert "Require order of request ID layers"
1da38bb
One layer to generate and inject the header
82marbag 75abca7
HeaderName for header name
82marbag 67e8d8f
CHANGELOG
82marbag 33bffbf
Remove additional layer
82marbag 2910853
Remove to_owned
82marbag 38d0302
Add tests, remove unnecessary clone
82marbag a244b9d
take() ResponsePackage instead
82marbag 842d0cc
Update docs
82marbag 24f58a3
Update docs
82marbag 5dd2670
cargo fmt
82marbag c41ba44
Update CHANGELOG
82marbag 4f7e150
Merge branch 'main' into request-id-response-headers
82marbag File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,8 @@ | |
//! | ||
//! The [`ServerRequestId`] is not meant to be propagated to downstream dependencies of the service. You should rely on a distributed tracing implementation for correlation purposes (e.g. OpenTelemetry). | ||
//! | ||
//! To optionally add the [`ServerRequestId`] to the response headers, use [`ServerRequestIdProviderLayer::new_with_response_header`]. | ||
//! | ||
//! ## Examples | ||
//! | ||
//! Your handler can now optionally take as input a [`ServerRequestId`]. | ||
|
@@ -34,7 +36,8 @@ | |
//! .operation(handler) | ||
//! .build().unwrap(); | ||
//! | ||
//! let app = app.layer(&ServerRequestIdProviderLayer::new()); /* Generate a server request ID */ | ||
//! let app = app | ||
//! .layer(&ServerRequestIdProviderLayer::new_with_response_header(HeaderName::from_static("x-request-id"))); /* Generate a server request ID and add it to the response header */ | ||
//! | ||
//! let bind: std::net::SocketAddr = format!("{}:{}", args.address, args.port) | ||
//! .parse() | ||
|
@@ -46,8 +49,11 @@ use std::{ | |
fmt::Display, | ||
task::{Context, Poll}, | ||
}; | ||
use std::future::Future; | ||
|
||
use futures_util::TryFuture; | ||
use http::request::Parts; | ||
use http::{header::HeaderName, HeaderValue, Response}; | ||
use thiserror::Error; | ||
use tower::{Layer, Service}; | ||
use uuid::Uuid; | ||
|
@@ -74,6 +80,10 @@ impl ServerRequestId { | |
pub fn new() -> Self { | ||
Self { id: Uuid::new_v4() } | ||
} | ||
|
||
pub(crate) fn to_header(&self) -> HeaderValue { | ||
HeaderValue::from_str(&self.id.to_string()).expect("This string contains only valid ASCII") | ||
} | ||
} | ||
|
||
impl Display for ServerRequestId { | ||
|
@@ -99,17 +109,25 @@ impl Default for ServerRequestId { | |
#[derive(Clone)] | ||
pub struct ServerRequestIdProvider<S> { | ||
inner: S, | ||
header_key: Option<HeaderName>, | ||
} | ||
|
||
/// A layer that provides services with a unique request ID instance | ||
#[derive(Debug)] | ||
#[non_exhaustive] | ||
pub struct ServerRequestIdProviderLayer; | ||
pub struct ServerRequestIdProviderLayer { | ||
header_key: Option<HeaderName>, | ||
} | ||
|
||
impl ServerRequestIdProviderLayer { | ||
/// Generate a new unique request ID | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd update this to mention this other constructor and explain that it won't add it to the response headers (and vice versa in the docs for the other method). |
||
pub fn new() -> Self { | ||
Self {} | ||
Self { header_key: None, } | ||
} | ||
|
||
/// Generate a new unique request ID and add it as a response header | ||
pub fn new_with_response_header(header_key: HeaderName) -> Self { | ||
Self { header_key: Some(header_key), } | ||
} | ||
} | ||
|
||
|
@@ -123,25 +141,44 @@ impl<S> Layer<S> for ServerRequestIdProviderLayer { | |
type Service = ServerRequestIdProvider<S>; | ||
|
||
fn layer(&self, inner: S) -> Self::Service { | ||
ServerRequestIdProvider { inner } | ||
ServerRequestIdProvider { inner, header_key: self.header_key.clone() } | ||
} | ||
} | ||
|
||
impl<Body, S> Service<http::Request<Body>> for ServerRequestIdProvider<S> | ||
where | ||
S: Service<http::Request<Body>>, | ||
S: Service<http::Request<Body>, Response = Response<crate::body::BoxBody>>, | ||
S::Future: std::marker::Send + 'static, | ||
{ | ||
type Response = S::Response; | ||
type Error = S::Error; | ||
type Future = S::Future; | ||
type Future = ServerRequestIdResponseFuture<S::Future>; | ||
|
||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { | ||
self.inner.poll_ready(cx) | ||
} | ||
|
||
fn call(&mut self, mut req: http::Request<Body>) -> Self::Future { | ||
req.extensions_mut().insert(ServerRequestId::new()); | ||
self.inner.call(req) | ||
let request_id = ServerRequestId::new(); | ||
match &self.header_key { | ||
Some(header_key) => { | ||
req.extensions_mut().insert(request_id.clone()); | ||
ServerRequestIdResponseFuture { | ||
response_package: Some(ResponsePackage { | ||
request_id, | ||
header_key: header_key.clone(), | ||
}), | ||
fut: self.inner.call(req), | ||
} | ||
} | ||
None => { | ||
req.extensions_mut().insert(request_id); | ||
ServerRequestIdResponseFuture { | ||
response_package: None, | ||
fut: self.inner.call(req), | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
@@ -150,3 +187,82 @@ impl<Protocol> IntoResponse<Protocol> for MissingServerRequestId { | |
internal_server_error() | ||
} | ||
} | ||
|
||
struct ResponsePackage { | ||
request_id: ServerRequestId, | ||
header_key: HeaderName, | ||
} | ||
|
||
pin_project_lite::pin_project! { | ||
pub struct ServerRequestIdResponseFuture<Fut> { | ||
response_package: Option<ResponsePackage>, | ||
#[pin] | ||
fut: Fut, | ||
} | ||
} | ||
|
||
impl<Fut> Future for ServerRequestIdResponseFuture<Fut> | ||
where | ||
Fut: TryFuture<Ok = Response<crate::body::BoxBody>>, | ||
{ | ||
type Output = Result<Fut::Ok, Fut::Error>; | ||
|
||
fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { | ||
let this = self.project(); | ||
let fut = this.fut; | ||
let response_package = this.response_package; | ||
fut.try_poll(cx) | ||
.map_ok(|mut res| { | ||
if let Some(response_package) = response_package.take() { | ||
res.headers_mut().insert(response_package.header_key, response_package.request_id.to_header()); | ||
} | ||
res | ||
}) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
use crate::body::{Body, BoxBody}; | ||
use crate::request::Request; | ||
use http::HeaderValue; | ||
use tower::{service_fn, ServiceBuilder, ServiceExt}; | ||
use std::convert::Infallible; | ||
|
||
#[test] | ||
fn test_request_id_parsed_by_header_value_infallible() { | ||
ServerRequestId::new().to_header(); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_request_id_in_response_header() { | ||
let svc = ServiceBuilder::new() | ||
.layer(&ServerRequestIdProviderLayer::new_with_response_header(HeaderName::from_static("x-request-id"))) | ||
.service(service_fn(|_req: Request<Body>| async move { | ||
Ok::<_, Infallible>(Response::new(BoxBody::default())) | ||
})); | ||
|
||
let req = Request::new(Body::empty()); | ||
|
||
let res = svc.oneshot(req).await.unwrap(); | ||
let request_id = res.headers().get("x-request-id").unwrap().to_str().unwrap(); | ||
|
||
assert!(HeaderValue::from_str(request_id).is_ok()); | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_request_id_not_in_response_header() { | ||
let svc = ServiceBuilder::new() | ||
.layer(&ServerRequestIdProviderLayer::new()) | ||
.service(service_fn(|_req: Request<Body>| async move { | ||
Ok::<_, Infallible>(Response::new(BoxBody::default())) | ||
})); | ||
|
||
let req = Request::new(Body::empty()); | ||
|
||
let res = svc.oneshot(req).await.unwrap(); | ||
|
||
assert!(res.headers().is_empty()); | ||
} | ||
} |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically, this is a breaking change: