Skip to content

Commit

Permalink
Don't print non-utf8 bodies (#125)
Browse files Browse the repository at this point in the history
* Don't print non-utf8 bodies

A lot of our services take in multipart data or binary data in the HTTP
body and when a wiremock predicate fails megabytes of binary data get
printed out to the console. Here I address this by not printing out a
string if it isn't valid utf8 and instead printing out the body length.

Another alternative may be to put in an upper limit to how large a body
will be printed and if it exceeds that size maybe saving the request to
a file for later analysis. But this change was so simple I figured I'd
open the PR first to start the discussion.

* Remove need for clone

* Apply some PR feature suggestions

1. Setting limit via env var or builder
2. Having a default length limit
3. Printed suggestion on upping the limit

* Update src/request.rs

Co-authored-by: Luca Palmieri <[email protected]>

* Apply feedback with BodyPrintLimit enum

* Expand to try and find valid printable byte

* Update src/request.rs

Co-authored-by: Luca Palmieri <[email protected]>

* Update src/mock_server/builder.rs

---------

Co-authored-by: Luca Palmieri <[email protected]>
  • Loading branch information
xd009642 and LukeMathWalker authored Nov 3, 2023
1 parent 443a585 commit 698cd76
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 4 deletions.
12 changes: 10 additions & 2 deletions src/mock_server/bare_server.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::mock_server::hyper::run_server;
use crate::mock_set::MockId;
use crate::mock_set::MountedMockSet;
use crate::request::BodyPrintLimit;
use crate::{mock::Mock, verification::VerificationOutcome, Request};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::pin::pin;
Expand Down Expand Up @@ -29,13 +30,15 @@ pub(crate) struct BareMockServer {
pub(super) struct MockServerState {
mock_set: MountedMockSet,
received_requests: Option<Vec<Request>>,
body_print_limit: BodyPrintLimit,
}

impl MockServerState {
pub(super) async fn handle_request(
&mut self,
request: Request,
mut request: Request,
) -> (http_types::Response, Option<futures_timer::Delay>) {
request.body_print_limit = self.body_print_limit;
// If request recording is enabled, record the incoming request
// by adding it to the `received_requests` stack
if let Some(received_requests) = &mut self.received_requests {
Expand All @@ -48,7 +51,11 @@ impl MockServerState {
impl BareMockServer {
/// Start a new instance of a `BareMockServer` listening on the specified
/// [`TcpListener`](std::net::TcpListener).
pub(super) async fn start(listener: TcpListener, request_recording: RequestRecording) -> Self {
pub(super) async fn start(
listener: TcpListener,
request_recording: RequestRecording,
body_print_limit: BodyPrintLimit,
) -> Self {
let (shutdown_trigger, shutdown_receiver) = tokio::sync::oneshot::channel();
let received_requests = match request_recording {
RequestRecording::Enabled => Some(Vec::new()),
Expand All @@ -57,6 +64,7 @@ impl BareMockServer {
let state = Arc::new(RwLock::new(MockServerState {
mock_set: MountedMockSet::new(),
received_requests,
body_print_limit,
}));
let server_address = listener
.local_addr()
Expand Down
25 changes: 24 additions & 1 deletion src/mock_server/builder.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
use crate::mock_server::bare_server::{BareMockServer, RequestRecording};
use crate::mock_server::exposed_server::InnerServer;
use crate::request::{BodyPrintLimit, BODY_PRINT_LIMIT};
use crate::MockServer;
use std::env;
use std::net::TcpListener;

/// A builder providing a fluent API to assemble a [`MockServer`] step-by-step.
/// Use [`MockServer::builder`] to get started.
pub struct MockServerBuilder {
listener: Option<TcpListener>,
record_incoming_requests: bool,
body_print_limit: BodyPrintLimit,
}

impl MockServerBuilder {
pub(super) fn new() -> Self {
let body_print_limit = match env::var("WIREMOCK_BODY_PRINT_LIMIT")
.ok()
.and_then(|x| x.parse::<usize>().ok())
{
Some(limit) => BodyPrintLimit::Limited(limit),
None => BodyPrintLimit::Limited(BODY_PRINT_LIMIT),
};
Self {
listener: None,
record_incoming_requests: true,
body_print_limit,
}
}

Expand Down Expand Up @@ -76,6 +87,18 @@ impl MockServerBuilder {
self
}

/// The mock server prints the requests it received when one or more mocks have expectations that have not been satisfied.
/// By default, the size of the printed body is limited.
///
/// You may want to change this if you're working with services with very large
/// bodies, or when printing wiremock output to a file where size matters
/// less than in a terminal window. You can configure this limit with
/// `MockServerBuilder::body_print_limit`.
pub fn body_print_limit(mut self, limit: BodyPrintLimit) -> Self {
self.body_print_limit = limit;
self
}

/// Finalise the builder to get an instance of a [`BareMockServer`].
pub(super) async fn build_bare(self) -> BareMockServer {
let listener = if let Some(listener) = self.listener {
Expand All @@ -88,7 +111,7 @@ impl MockServerBuilder {
} else {
RequestRecording::Disabled
};
BareMockServer::start(listener, recording).await
BareMockServer::start(listener, recording, self.body_print_limit).await
}

/// Finalise the builder and launch the [`MockServer`] instance!
Expand Down
57 changes: 56 additions & 1 deletion src/request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ use http_types::convert::DeserializeOwned;
use http_types::headers::{HeaderName, HeaderValue, HeaderValues};
use http_types::{Method, Url};

pub const BODY_PRINT_LIMIT: usize = 10_000;

/// Specifies limitations on printing request bodies when logging requests. For some mock servers
/// the bodies may be too large to reasonably print and it may be desireable to limit them.
#[derive(Debug, Copy, Clone)]
pub enum BodyPrintLimit {
/// Maximum length of a body to print in bytes.
Limited(usize),
/// There is no limit to the size of a body that may be printed.
Unlimited,
}

/// An incoming request to an instance of [`MockServer`].
///
/// Each matcher gets an immutable reference to a `Request` instance in the [`matches`] method
Expand All @@ -31,6 +43,7 @@ pub struct Request {
pub method: Method,
pub headers: HashMap<HeaderName, HeaderValues>,
pub body: Vec<u8>,
pub body_print_limit: BodyPrintLimit,
}

impl fmt::Display for Request {
Expand All @@ -44,7 +57,47 @@ impl fmt::Display for Request {
let values = values.join(",");
writeln!(f, "{}: {}", name, values)?;
}
writeln!(f, "{}", String::from_utf8_lossy(&self.body))

match self.body_print_limit {
BodyPrintLimit::Limited(limit) if self.body.len() > limit => {
let mut written = false;
for end_byte in limit..(limit + 4).max(self.body.len()) {
if let Ok(truncated) = std::str::from_utf8(&self.body[..end_byte]) {
written = true;
writeln!(f, "{}", truncated)?;
if end_byte < self.body.len() {
writeln!(
f,
"We truncated the body because it was too large: {} bytes (limit: {} bytes)",
self.body.len(),
limit
)?;
writeln!(f, "Increase this limit by setting `WIREMOCK_BODY_PRINT_LIMIT`, or calling `MockServerBuilder::body_print_limit` when building your MockServer instance")?;
}
}
}
if !written {
writeln!(
f,
"Body is likely binary (invalid utf-8) size is {} bytes",
self.body.len()
)
} else {
Ok(())
}
}
_ => {
if let Ok(body) = std::str::from_utf8(&self.body) {
writeln!(f, "{}", body)
} else {
writeln!(
f,
"Body is likely binary (invalid utf-8) size is {} bytes",
self.body.len()
)
}
}
}
}
}

Expand Down Expand Up @@ -75,6 +128,7 @@ impl Request {
method,
headers,
body,
body_print_limit: BodyPrintLimit::Limited(BODY_PRINT_LIMIT),
}
}

Expand Down Expand Up @@ -119,6 +173,7 @@ impl Request {
method,
headers,
body,
body_print_limit: BodyPrintLimit::Limited(BODY_PRINT_LIMIT),
}
}
}

0 comments on commit 698cd76

Please sign in to comment.