From 698cd76da311b0d9c6a30abc5734ced87fe5971d Mon Sep 17 00:00:00 2001 From: xd009642 Date: Fri, 3 Nov 2023 12:30:05 +0000 Subject: [PATCH] Don't print non-utf8 bodies (#125) * 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 <20745048+LukeMathWalker@users.noreply.github.com> * Apply feedback with BodyPrintLimit enum * Expand to try and find valid printable byte * Update src/request.rs Co-authored-by: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com> * Update src/mock_server/builder.rs --------- Co-authored-by: Luca Palmieri <20745048+LukeMathWalker@users.noreply.github.com> --- src/mock_server/bare_server.rs | 12 +++++-- src/mock_server/builder.rs | 25 ++++++++++++++- src/request.rs | 57 +++++++++++++++++++++++++++++++++- 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/mock_server/bare_server.rs b/src/mock_server/bare_server.rs index e8877a0..c13a82b 100644 --- a/src/mock_server/bare_server.rs +++ b/src/mock_server/bare_server.rs @@ -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; @@ -29,13 +30,15 @@ pub(crate) struct BareMockServer { pub(super) struct MockServerState { mock_set: MountedMockSet, received_requests: Option>, + body_print_limit: BodyPrintLimit, } impl MockServerState { pub(super) async fn handle_request( &mut self, - request: Request, + mut request: Request, ) -> (http_types::Response, Option) { + 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 { @@ -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()), @@ -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() diff --git a/src/mock_server/builder.rs b/src/mock_server/builder.rs index eb70266..69581c4 100644 --- a/src/mock_server/builder.rs +++ b/src/mock_server/builder.rs @@ -1,6 +1,8 @@ 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. @@ -8,13 +10,22 @@ use std::net::TcpListener; pub struct MockServerBuilder { listener: Option, 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::().ok()) + { + Some(limit) => BodyPrintLimit::Limited(limit), + None => BodyPrintLimit::Limited(BODY_PRINT_LIMIT), + }; Self { listener: None, record_incoming_requests: true, + body_print_limit, } } @@ -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 { @@ -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! diff --git a/src/request.rs b/src/request.rs index cb2e163..8e078ef 100644 --- a/src/request.rs +++ b/src/request.rs @@ -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 @@ -31,6 +43,7 @@ pub struct Request { pub method: Method, pub headers: HashMap, pub body: Vec, + pub body_print_limit: BodyPrintLimit, } impl fmt::Display for Request { @@ -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() + ) + } + } + } } } @@ -75,6 +128,7 @@ impl Request { method, headers, body, + body_print_limit: BodyPrintLimit::Limited(BODY_PRINT_LIMIT), } } @@ -119,6 +173,7 @@ impl Request { method, headers, body, + body_print_limit: BodyPrintLimit::Limited(BODY_PRINT_LIMIT), } } }