1use std::io::Read;
2
3use anyhow::Result;
4use brotli::Decompressor;
5use bytes::Bytes;
6use chrono::{
7 DateTime,
8 Utc,
9};
10use httparse::Header;
11use tokio::task;
12use tracing::{
13 debug,
14 trace,
15};
16
17use crate::message::LogMessage;
18use crate::parser::{
19 BodyParser,
20 GzipMembers,
21 RequestParser,
22 ResponseParser,
23};
24
25const MAX_DECODED_BODY_SIZE: usize = 10 * 1024 * 1024;
26
27pub struct MessageFormatter;
28
29impl MessageFormatter {
30 pub async fn format_request(
31 buffer: &Bytes, trace_id: &str, timestamp: DateTime<Utc>,
32 ) -> Result<LogMessage> {
33 debug!("Formatting request with trace ID: {}", trace_id);
34
35 let mut log_entry = Self::create_metadata_header(trace_id, timestamp, None);
36
37 let (method, path, version, headers) = RequestParser::parse(buffer)?;
38
39 if let (Some(method), Some(path), Some(version)) = (method, path, version) {
40 log_entry.push_str(&format!("{method} {path} HTTP/1.{version}\n"));
41
42 Self::append_headers(&headers, &mut log_entry);
43
44 log_entry.push('\n');
45
46 if let Some(body) = RequestParser::extract_body(buffer) {
47 Self::format_body(body, &headers, &mut log_entry).await?;
48 Self::append_log_separator(&mut log_entry);
49 } else {
50 Self::append_log_separator(&mut log_entry);
51 }
52 }
53
54 Ok(LogMessage::Request(log_entry))
55 }
56
57 pub async fn format_response(
58 buffer: &Bytes, trace_id: &str, timestamp: DateTime<Utc>, took: i64,
59 ) -> Result<LogMessage> {
60 debug!("Formatting response with trace ID: {}", trace_id);
61
62 let mut log_entry = Self::create_metadata_header(trace_id, timestamp, Some(took));
63
64 let (status_code, headers) = ResponseParser::parse(buffer)?;
65
66 if let Some(status) = status_code {
67 log_entry.push_str(&format!(
68 "HTTP/1.1 {} {}\n",
69 status,
70 Self::status_text(status)
71 ));
72
73 Self::append_headers(&headers, &mut log_entry);
74 log_entry.push('\n');
75
76 if let Some(body) = RequestParser::extract_body(buffer) {
77 let processed_body = Self::process_response_body(body, &headers);
78 Self::format_body(&processed_body, &headers, &mut log_entry).await?;
79 Self::append_log_separator(&mut log_entry);
80 } else {
81 Self::append_log_separator(&mut log_entry);
82 }
83 }
84
85 Ok(LogMessage::Response(log_entry))
86 }
87
88 pub fn format_preformatted_response(
89 trace_id: &str, timestamp: DateTime<Utc>, took: i64, buffer: &Bytes,
90 ) -> String {
91 debug!(
92 "Formatting preformatted response with trace ID: {}",
93 trace_id
94 );
95
96 let mut log_entry = Self::create_metadata_header(trace_id, timestamp, Some(took));
97
98 if buffer.len() > 5 && &buffer[0..5] == b"HTTP/" {
99 if let Some(headers_end) = Self::find_headers_end(buffer) {
100 let headers_bytes = &buffer[..headers_end];
101 let body_bytes = &buffer[headers_end..];
102
103 if let Ok(headers_str) = std::str::from_utf8(headers_bytes) {
104 log_entry.push_str(headers_str);
105 log_entry.push('\n');
106 } else {
107 log_entry.push_str("HTTP/1.1 200 OK\n<unparseable headers>\n\n");
108 }
109
110 if !body_bytes.is_empty() {
111 let mut content_type = None;
112 let mut content_encodings = Vec::new();
113 let mut is_chunked = false;
114
115 if let Ok(headers_str) = std::str::from_utf8(headers_bytes) {
116 for line in headers_str.lines() {
117 let line_lower = line.to_lowercase();
118 if line_lower.starts_with("content-type:") {
119 content_type = line.split(':').nth(1).map(|s| s.trim());
120 }
121 if let Some(value) = line_lower.strip_prefix("content-encoding:") {
122 content_encodings.push(value.trim().to_string());
123 }
124 if line_lower.starts_with("transfer-encoding:")
125 && line_lower.contains("chunked")
126 {
127 is_chunked = true;
128 }
129 }
130 }
131
132 debug!(
133 "Content-Type: {:?}, Content-Encoding: {:?}, chunked: {}",
134 content_type, content_encodings, is_chunked
135 );
136
137 let mut processed_body = if is_chunked {
138 debug!("Dechunking body");
139 Self::dechunk_body(body_bytes)
140 } else {
141 body_bytes.to_vec()
142 };
143
144 let mut truncated = false;
145 let encodings = content_encodings.iter().rev().flat_map(|v| v.rsplit(','));
146 for encoding in encodings.map(str::trim) {
147 let decoded = match encoding {
148 "gzip" => Self::decompress_gzip(&processed_body),
149 "deflate" => Self::decompress_deflate(&processed_body),
150 "br" => {
151 Self::read_bounded(Decompressor::new(&processed_body[..], 4096))
152 }
153 _ => continue,
154 };
155 match decoded {
156 Ok((decompressed, hit_limit)) => {
157 debug!(
158 "Decoded {} content: {} -> {} bytes",
159 encoding,
160 processed_body.len(),
161 decompressed.len()
162 );
163 processed_body = decompressed;
164 if hit_limit {
165 truncated = true;
166 break;
167 }
168 }
169 Err(e) => {
170 debug!("Failed to decode {} content: {:?}", encoding, e);
171 }
172 }
173 }
174
175 let content_type_str = content_type.unwrap_or("text/plain");
176 Self::format_content(&mut log_entry, content_type_str, &processed_body);
177 if truncated {
178 log_entry.push_str(&format!(
179 "\n# <body truncated at {MAX_DECODED_BODY_SIZE} bytes after decoding>"
180 ));
181 }
182 }
183 } else if let Ok(content) = std::str::from_utf8(buffer) {
184 log_entry.push_str(content);
185 } else {
186 log_entry.push_str("<binary content>");
187 }
188 } else if let Ok(content) = std::str::from_utf8(buffer) {
189 log_entry.push_str(content);
190 } else {
191 log_entry.push_str("<binary content>");
192 }
193
194 Self::append_log_separator(&mut log_entry);
195 log_entry
196 }
197
198 fn format_content(log_entry: &mut String, content_type: &str, body: &[u8]) {
199 if body.is_empty() {
200 return;
201 }
202
203 let content_type_lower = content_type.to_lowercase();
204
205 if content_type_lower.contains("javascript") {
206 debug!("Formatting as JavaScript");
207 log_entry.push_str(&String::from_utf8_lossy(body));
208 return;
209 }
210
211 if content_type_lower.contains("json") {
212 debug!("Attempting to format as JSON");
213 if let Ok(text) = std::str::from_utf8(body) {
214 if let Ok(json) = serde_json::from_str::<serde_json::Value>(text.trim())
215 && let Ok(pretty) = serde_json::to_string_pretty(&json)
216 {
217 log_entry.push_str(&pretty);
218 return;
219 }
220 log_entry.push_str(text);
221 } else {
222 log_entry.push_str(&String::from_utf8_lossy(body));
223 }
224 return;
225 }
226
227 match content_type_lower {
228 t if t.contains("html") => {
229 debug!("Formatting as HTML");
230 log_entry.push_str(&String::from_utf8_lossy(body));
231 }
232 t if t.starts_with("text/") => {
233 debug!("Formatting as text");
234 log_entry.push_str(&String::from_utf8_lossy(body));
235 }
236 t if t.contains("xml") => {
237 debug!("Formatting as XML");
238 log_entry.push_str(&String::from_utf8_lossy(body));
239 }
240 _ => {
241 if body
242 .iter()
243 .any(|&b| b < 32 && b != b'\n' && b != b'\r' && b != b'\t')
244 {
245 debug!("Detected binary content");
246 log_entry.push_str(&format!("# <binary content: {} bytes>", body.len()));
247 } else {
248 log_entry.push_str(&String::from_utf8_lossy(body));
249 }
250 }
251 }
252 }
253
254 fn decompress_gzip(data: &[u8]) -> Result<(Vec<u8>, bool)> {
255 if data.len() < 2 || data[0] != 0x1f || data[1] != 0x8b {
256 return Err(anyhow::anyhow!("Not a valid gzip header"));
257 }
258
259 debug!("Decompressing gzip data of {} bytes", data.len());
260
261 let decompressed = Self::read_bounded(GzipMembers::new(data))?;
262
263 if decompressed.0.is_empty() {
264 return Err(anyhow::anyhow!("Decompression produced empty result"));
265 }
266
267 Ok(decompressed)
268 }
269
270 fn decompress_deflate(data: &[u8]) -> Result<(Vec<u8>, bool)> {
271 Self::read_bounded(flate2::read::ZlibDecoder::new(data))
272 .or_else(|_| Self::read_bounded(flate2::read::DeflateDecoder::new(data)))
273 }
274
275 fn read_bounded(reader: impl Read) -> Result<(Vec<u8>, bool)> {
276 let mut decoded = Vec::new();
277 reader
278 .take(MAX_DECODED_BODY_SIZE as u64 + 1)
279 .read_to_end(&mut decoded)?;
280 let hit_limit = decoded.len() > MAX_DECODED_BODY_SIZE;
281 decoded.truncate(MAX_DECODED_BODY_SIZE);
282 Ok((decoded, hit_limit))
283 }
284
285 fn dechunk_body(body: &[u8]) -> Vec<u8> {
286 if body.is_empty() {
287 return Vec::new();
288 }
289
290 let mut result = Vec::new();
291 let mut pos = 0;
292
293 while pos < body.len() && (body[pos] == b' ' || body[pos] == b'\r' || body[pos] == b'\n') {
294 pos += 1;
295 }
296
297 while pos < body.len() {
298 let chunk_size_end_pos = match body[pos..].iter().position(|&b| b == b'\r') {
299 Some(p) => pos + p,
300 None => {
301 debug!("No CRLF found in chunked data, returning partial result");
302 return result;
303 }
304 };
305
306 if chunk_size_end_pos + 1 >= body.len() || body[chunk_size_end_pos + 1] != b'\n' {
307 debug!("Invalid chunk format (missing LF after CR)");
308 return result;
309 }
310
311 let chunk_size_str = match std::str::from_utf8(&body[pos..chunk_size_end_pos]) {
312 Ok(s) => s.trim(),
313 Err(_) => {
314 debug!("Invalid UTF-8 in chunk size");
315 return result;
316 }
317 };
318
319 let chunk_size_str = chunk_size_str.split(';').next().unwrap_or("").trim();
320
321 let chunk_size = match usize::from_str_radix(chunk_size_str, 16) {
322 Ok(size) => size,
323 Err(e) => {
324 debug!("Failed to parse chunk size '{}': {}", chunk_size_str, e);
325 return result;
326 }
327 };
328
329 if chunk_size == 0 {
330 debug!("Found end chunk marker");
331 break;
332 }
333
334 let chunk_data_start = chunk_size_end_pos + 2;
335
336 if chunk_data_start + chunk_size + 2 > body.len() {
337 debug!(
338 "Incomplete chunk: need {} bytes, have {}",
339 chunk_size,
340 body.len() - chunk_data_start
341 );
342
343 if chunk_data_start < body.len() {
344 result.extend_from_slice(&body[chunk_data_start..]);
345 }
346 break;
347 }
348
349 result.extend_from_slice(&body[chunk_data_start..(chunk_data_start + chunk_size)]);
350
351 pos = chunk_data_start + chunk_size + 2;
352 }
353
354 result
355 }
356
357 fn create_metadata_header(
358 trace_id: &str, timestamp: DateTime<Utc>, took_ms: Option<i64>,
359 ) -> String {
360 let initial_capacity = 128 + trace_id.len() + 64;
361 let mut header = String::with_capacity(initial_capacity);
362
363 header.push_str("\n# ----------------------------------------\n");
364 header.push_str("# Trace ID: ");
365 header.push_str(trace_id);
366 header.push('\n');
367
368 if let Some(took) = took_ms {
369 header.push_str("# Response at: ");
370 header.push_str(×tamp.to_rfc3339());
371 header.push('\n');
372 header.push_str("# Took: ");
373 header.push_str(&took.to_string());
374 header.push_str(" ms\n");
375 } else {
376 header.push_str("# Request at: ");
377 header.push_str(×tamp.to_rfc3339());
378 header.push('\n');
379 }
380
381 header
382 }
383
384 fn append_headers(headers: &[Header<'_>], log_entry: &mut String) {
385 let additional_capacity = headers
386 .iter()
387 .map(|h| h.name.len() + h.value.len() + 3)
388 .sum::<usize>();
389
390 let current_len = log_entry.len();
391 if log_entry.capacity() < current_len + additional_capacity {
392 log_entry.reserve(additional_capacity);
393 }
394
395 for header in headers {
396 log_entry.push_str(header.name);
397 log_entry.push_str(": ");
398 if let Ok(value) = std::str::from_utf8(header.value) {
399 log_entry.push_str(value);
400 }
401 log_entry.push('\n');
402 }
403 }
404
405 fn process_response_body(body: &[u8], headers: &[Header<'_>]) -> Vec<u8> {
406 let is_chunked = RequestParser::is_chunked_transfer(headers);
407
408 if !is_chunked {
409 return body.to_vec();
410 }
411
412 trace!(
413 "Processing chunked transfer encoded body of {} bytes",
414 body.len()
415 );
416
417 let processed_body = RequestParser::process_chunked_body(body);
418
419 if processed_body.len() != body.len() {
420 trace!(
421 "Chunked body processed: {} bytes original, {} bytes after dechunking",
422 body.len(),
423 processed_body.len()
424 );
425 }
426
427 processed_body
428 }
429
430 fn append_log_separator(log_entry: &mut String) {
431 log_entry.push_str("\n\n###\n");
432 }
433
434 async fn format_body(
435 body: &[u8], headers: &[Header<'_>], log_entry: &mut String,
436 ) -> Result<()> {
437 let content_length = RequestParser::get_content_length(headers);
438
439 if BodyParser::is_content_too_large(content_length) {
440 log_entry.push_str("<content too large>");
441 return Ok(());
442 }
443
444 let (body_content, truncated) = match Self::try_decompress_body(body, headers).await {
445 Ok(decoded) => decoded,
446 Err(e) => (
447 Self::handle_decompression_error(body, headers, log_entry, e)?,
448 false,
449 ),
450 };
451
452 let content_type = Self::extract_content_type(headers);
453 let formatted_body = BodyParser::format_body(&body_content, content_type)?;
454 log_entry.push_str(&formatted_body);
455 if truncated {
456 log_entry.push_str(&format!(
457 "\n# <body truncated at {MAX_DECODED_BODY_SIZE} bytes after decoding>"
458 ));
459 }
460
461 Ok(())
462 }
463
464 fn extract_content_type<'a>(headers: &'a [Header<'_>]) -> Option<&'a str> {
465 headers.iter().find_map(|h| {
466 if h.name.eq_ignore_ascii_case("content-type") {
467 std::str::from_utf8(h.value).ok()
468 } else {
469 None
470 }
471 })
472 }
473
474 fn handle_decompression_error(
475 body: &[u8], headers: &[Header<'_>], log_entry: &mut String, error: anyhow::Error,
476 ) -> Result<Vec<u8>> {
477 log_entry.push_str(&format!(
478 "<!-- Debug: Failed to process content: {error} -->"
479 ));
480
481 if let Some(content_type) = Self::extract_content_type(headers)
482 && content_type.contains("javascript")
483 {
484 Self::insert_js_placeholder(content_type, log_entry);
485 return Ok(Vec::new());
486 }
487
488 if let Ok(body_str) = std::str::from_utf8(body)
489 && !body_str.is_empty()
490 {
491 log_entry.push_str(body_str);
492 return Ok(Vec::new());
493 }
494
495 Ok(body.to_vec())
496 }
497
498 fn insert_js_placeholder(content_type: &str, log_entry: &mut String) {
499 log_entry.push_str("/**\n * This JavaScript content could not be fully decompressed.\n * Original response was compressed with gzip and chunked transfer encoding.\n * \n * The HTTP logging system captured the content but could not properly decompress it.\n * This is a placeholder to maintain HTTP log file compatibility with REST clients.\n */\n\n// Content metadata:\n// Content-Type: ");
500 log_entry.push_str(content_type);
501 log_entry.push_str("\n// Content-Encoding: gzip\n// Transfer-Encoding: chunked\n\n");
502 log_entry
503 .push_str("console.log('Content placeholder for compressed and chunked JavaScript');");
504 }
505
506 async fn try_decompress_body(body: &[u8], headers: &[Header<'_>]) -> Result<(Vec<u8>, bool)> {
507 if body.is_empty() || body.len() < 16 {
508 return Ok((body.to_vec(), false));
509 }
510
511 let content_encoding = headers.iter().find_map(|h| {
512 if h.name.eq_ignore_ascii_case("content-encoding") {
513 std::str::from_utf8(h.value).ok()
514 } else {
515 None
516 }
517 });
518
519 if content_encoding.is_none() {
520 return Ok((body.to_vec(), false));
521 }
522
523 let content_encoding = content_encoding.unwrap().to_lowercase();
524
525 let mut current_data = body.to_vec();
526 let mut truncated = false;
527
528 let encodings: Vec<&str> = content_encoding
529 .split(',')
530 .map(|s| s.trim())
531 .rev()
532 .collect();
533
534 for encoding in encodings {
535 let decoded = match encoding {
536 "gzip" => Self::decompress_gzip_async(¤t_data).await,
537 "br" => Self::try_decompress_brotli(¤t_data).await,
538 "deflate" => Self::try_decompress_deflate(¤t_data).await,
539 "identity" => {
540 debug!("Identity encoding - no decompression needed");
541 continue;
542 }
543 _ => {
544 debug!("Unknown content encoding: {}, keeping original", encoding);
545 continue;
546 }
547 };
548 match decoded {
549 Ok((decompressed, hit_limit)) if !decompressed.is_empty() => {
550 debug!(
551 "Successfully decompressed {} data: {} bytes -> {} bytes",
552 encoding,
553 current_data.len(),
554 decompressed.len()
555 );
556 current_data = decompressed;
557 if hit_limit {
558 truncated = true;
559 break;
560 }
561 }
562 Ok(_) => {
563 debug!(
564 "{} decompression produced empty result, keeping original",
565 encoding
566 );
567 }
568 Err(e) => {
569 debug!(
570 "{} decompression failed: {:?}, keeping original",
571 encoding, e
572 );
573 }
574 }
575 }
576
577 Ok((current_data, truncated))
578 }
579
580 async fn decompress_gzip_async(data: &[u8]) -> Result<(Vec<u8>, bool)> {
581 let data_owned = data.to_vec();
582 task::spawn_blocking(move || Self::read_bounded(GzipMembers::new(&data_owned[..]))).await?
583 }
584
585 async fn try_decompress_brotli(body: &[u8]) -> Result<(Vec<u8>, bool)> {
586 let body_owned = body.to_vec();
587 task::spawn_blocking(move || Self::read_bounded(Decompressor::new(&body_owned[..], 4096)))
588 .await?
589 }
590
591 async fn try_decompress_deflate(body: &[u8]) -> Result<(Vec<u8>, bool)> {
592 let body_owned = body.to_vec();
593 task::spawn_blocking(move || Self::decompress_deflate(&body_owned)).await?
594 }
595
596 fn find_headers_end(data: &[u8]) -> Option<usize> {
597 let mut i = 0;
598 while i + 3 < data.len() {
599 if data[i] == b'\r'
600 && data[i + 1] == b'\n'
601 && data[i + 2] == b'\r'
602 && data[i + 3] == b'\n'
603 {
604 return Some(i + 4);
605 }
606 i += 1;
607 }
608
609 let mut i = 0;
610 while i + 1 < data.len() {
611 if data[i] == b'\n' && data[i + 1] == b'\n' {
612 return Some(i + 2);
613 }
614 i += 1;
615 }
616
617 None
618 }
619
620 fn status_text(status: u16) -> &'static str {
621 match status {
622 100 => "Continue",
623 101 => "Switching Protocols",
624 102 => "Processing",
625 200 => "OK",
626 201 => "Created",
627 202 => "Accepted",
628 203 => "Non-Authoritative Information",
629 204 => "No Content",
630 205 => "Reset Content",
631 206 => "Partial Content",
632 207 => "Multi-Status",
633 300 => "Multiple Choices",
634 301 => "Moved Permanently",
635 302 => "Found",
636 303 => "See Other",
637 304 => "Not Modified",
638 305 => "Use Proxy",
639 307 => "Temporary Redirect",
640 308 => "Permanent Redirect",
641 400 => "Bad Request",
642 401 => "Unauthorized",
643 402 => "Payment Required",
644 403 => "Forbidden",
645 404 => "Not Found",
646 405 => "Method Not Allowed",
647 406 => "Not Acceptable",
648 407 => "Proxy Authentication Required",
649 408 => "Request Timeout",
650 409 => "Conflict",
651 410 => "Gone",
652 411 => "Length Required",
653 412 => "Precondition Failed",
654 413 => "Payload Too Large",
655 414 => "URI Too Long",
656 415 => "Unsupported Media Type",
657 416 => "Range Not Satisfiable",
658 417 => "Expectation Failed",
659 418 => "I'm a teapot",
660 422 => "Unprocessable Entity",
661 423 => "Locked",
662 424 => "Failed Dependency",
663 426 => "Upgrade Required",
664 428 => "Precondition Required",
665 429 => "Too Many Requests",
666 431 => "Request Header Fields Too Large",
667 451 => "Unavailable For Legal Reasons",
668 500 => "Internal Server Error",
669 501 => "Not Implemented",
670 502 => "Bad Gateway",
671 503 => "Service Unavailable",
672 504 => "Gateway Timeout",
673 505 => "HTTP Version Not Supported",
674 506 => "Variant Also Negotiates",
675 507 => "Insufficient Storage",
676 508 => "Loop Detected",
677 510 => "Not Extended",
678 511 => "Network Authentication Required",
679 _ => "Unknown Status",
680 }
681 }
682}
683
684#[test]
685fn test_format_content_json_pretty() {
686 let mut log_entry = String::new();
687 let json_body = b"{\"name\":\"test\", \"value\":123}";
688 MessageFormatter::format_content(&mut log_entry, "application/json", json_body);
689 let expected = "{\n \"name\": \"test\",\n \"value\": 123\n}";
690 assert_eq!(log_entry.trim(), expected.trim());
691}
692
693#[test]
694fn test_format_content_json_with_charset() {
695 let mut log_entry = String::new();
696 let json_body = b"{\"name\":\"test\"}";
697 MessageFormatter::format_content(&mut log_entry, "application/json; charset=utf-8", json_body);
698 let expected = "{\n \"name\": \"test\"\n}";
699 assert_eq!(log_entry.trim(), expected.trim());
700}
701
702#[test]
703fn test_format_content_javascript() {
704 let mut log_entry = String::new();
705 let js_body = b"function test() { console.log('hello'); }";
706 MessageFormatter::format_content(&mut log_entry, "application/javascript", js_body);
707 assert_eq!(log_entry, "function test() { console.log('hello'); }");
708
709 log_entry.clear();
710 MessageFormatter::format_content(&mut log_entry, "text/javascript", js_body);
711 assert_eq!(log_entry, "function test() { console.log('hello'); }");
712}
713
714#[test]
715fn test_format_content_html() {
716 let mut log_entry = String::new();
717 let html_body = b"<html><body><h1>Title</h1></body></html>";
718 MessageFormatter::format_content(&mut log_entry, "text/html", html_body);
719 assert_eq!(log_entry, "<html><body><h1>Title</h1></body></html>");
720}
721
722#[test]
723fn test_format_content_xml() {
724 let mut log_entry = String::new();
725 let xml_body = b"<root><item>value</item></root>";
726 MessageFormatter::format_content(&mut log_entry, "application/xml", xml_body);
727 assert_eq!(log_entry, "<root><item>value</item></root>");
728
729 log_entry.clear();
730 MessageFormatter::format_content(&mut log_entry, "text/xml", xml_body);
731 assert_eq!(log_entry, "<root><item>value</item></root>");
732}
733
734#[test]
735fn test_format_content_plain_text() {
736 let mut log_entry = String::new();
737 let text_body = b"Just some plain text.";
738 MessageFormatter::format_content(&mut log_entry, "text/plain", text_body);
739 assert_eq!(log_entry, "Just some plain text.");
740}
741
742#[test]
743fn test_format_content_other_text() {
744 let mut log_entry = String::new();
745 let csv_body = b"col1,col2\nval1,val2";
746 MessageFormatter::format_content(&mut log_entry, "text/csv", csv_body);
747 assert_eq!(log_entry, "col1,col2\nval1,val2");
748}
749
750#[test]
751fn test_format_content_binary() {
752 let mut log_entry = String::new();
753 let binary_body = &[0x01, 0x02, 0x03, 0x00, 0x1f, 0x8b];
754 MessageFormatter::format_content(&mut log_entry, "application/octet-stream", binary_body);
755 assert_eq!(
756 log_entry,
757 format!("# <binary content: {} bytes>", binary_body.len())
758 );
759}
760
761#[test]
762fn test_format_content_pseudo_binary_text() {
763 let mut log_entry = String::new();
764 let text_body = "Contains \t tab and \n newline and \r CR".as_bytes();
765 MessageFormatter::format_content(&mut log_entry, "application/unknown", text_body);
766 assert_eq!(log_entry, "Contains \t tab and \n newline and \r CR");
767}
768
769#[test]
770fn test_format_content_empty_body() {
771 let mut log_entry = String::new();
772 let empty_body = b"";
773 MessageFormatter::format_content(&mut log_entry, "text/plain", empty_body);
774 assert!(log_entry.is_empty());
775
776 log_entry.clear();
777 MessageFormatter::format_content(&mut log_entry, "application/json", empty_body);
778 assert!(log_entry.is_empty());
779}
780
781#[cfg(test)]
782mod formatter_tests {
783 use bytes::Bytes;
784 use chrono::Utc;
785 use httparse::Header;
786
787 use super::*;
788
789 #[tokio::test]
790 async fn test_decompress_gzip_async() {
791 let test_data = b"test text";
792 let mut encoder = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
793 std::io::Write::write_all(&mut encoder, test_data).unwrap();
794 let gzip_data = encoder.finish().unwrap();
795
796 assert!(gzip_data.len() > 2 && gzip_data[0] == 0x1f && gzip_data[1] == 0x8b);
797
798 let result = MessageFormatter::decompress_gzip_async(&gzip_data).await;
799 assert!(
800 result.is_ok(),
801 "Gzip decompression failed: {:?}",
802 result.err()
803 );
804 let decompressed = result.unwrap();
805 assert_eq!(decompressed, (test_data.to_vec(), false));
806 }
807
808 #[tokio::test]
809 async fn test_decompress_gzip_invalid_header() {
810 let invalid_data = vec![0x01, 0x02, 0x03, 0x04, 0x05];
811
812 let result = MessageFormatter::decompress_gzip(&invalid_data);
813 assert!(result.is_err());
814 }
815
816 #[tokio::test]
817 async fn test_try_decompress_brotli() {
818 let test_data = b"hello world";
819 let mut encoder = brotli::CompressorReader::new(test_data.as_slice(), 4096, 11, 22);
820 let mut compressed = Vec::new();
821 std::io::copy(&mut encoder, &mut compressed).unwrap();
822
823 let result = MessageFormatter::try_decompress_brotli(&compressed).await;
824 assert!(
825 result.is_ok(),
826 "Brotli decompression failed: {:?}",
827 result.err()
828 );
829 let decompressed = result.unwrap();
830 assert_eq!(decompressed, (test_data.to_vec(), false));
831 }
832
833 #[tokio::test]
834 async fn test_try_decompress_deflate() {
835 let test_data = b"test data for deflate";
836 let mut encoder =
837 flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
838 std::io::Write::write_all(&mut encoder, test_data).unwrap();
839 let deflate_data = encoder.finish().unwrap();
840
841 let result = MessageFormatter::try_decompress_deflate(&deflate_data).await;
842 assert!(
843 result.is_ok(),
844 "Deflate decompression failed: {:?}",
845 result.err()
846 );
847 let decompressed = result.unwrap();
848 assert_eq!(decompressed, (test_data.to_vec(), false));
849 }
850
851 #[tokio::test]
852 async fn test_format_request_zlib_deflate_body() {
853 let item = r#"{"user":"alice","note":"zlib check"}"#;
854 let json = format!("[{}]", vec![item; 20].join(","));
855 let mut encoder =
856 flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
857 std::io::Write::write_all(&mut encoder, json.as_bytes()).unwrap();
858 let body = encoder.finish().unwrap();
859 let mut request = format!(
860 "POST /api HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\nContent-Encoding: \
861 deflate\r\nContent-Length: {}\r\n\r\n",
862 body.len()
863 )
864 .into_bytes();
865 request.extend_from_slice(&body);
866
867 let LogMessage::Request(text) =
868 MessageFormatter::format_request(&Bytes::from(request), "t1", Utc::now())
869 .await
870 .unwrap()
871 else {
872 panic!("not a request")
873 };
874
875 assert!(text.contains("\"user\": \"alice\""), "{text}");
876 assert!(!text.contains("invalid JSON"), "{text}");
877 }
878
879 #[tokio::test]
880 async fn test_format_request_caps_decoded_body() {
881 let kept = "a".repeat(MAX_DECODED_BODY_SIZE);
882 let payload = format!("{kept}a");
883 let mut zlib = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
884 std::io::Write::write_all(&mut zlib, payload.as_bytes()).unwrap();
885
886 for (encoding, body) in [
887 ("br", brotli_compress(payload.as_bytes())),
888 ("gzip", gzip_compress(payload.as_bytes())),
889 ("deflate", zlib.finish().unwrap()),
890 ] {
891 let mut request = format!(
892 "POST /upload HTTP/1.1\r\nHost: x\r\nContent-Type: text/plain\r\nContent-Encoding: \
893 {encoding}\r\nContent-Length: {}\r\n\r\n",
894 body.len()
895 )
896 .into_bytes();
897 request.extend_from_slice(&body);
898
899 let LogMessage::Request(text) =
900 MessageFormatter::format_request(&Bytes::from(request), "t1", Utc::now())
901 .await
902 .unwrap()
903 else {
904 panic!("not a request")
905 };
906
907 assert!(text.contains(&kept), "encoding {encoding}");
908 assert!(!text.contains(&payload), "encoding {encoding}");
909 assert!(
910 text.contains(&format!(
911 "# <body truncated at {MAX_DECODED_BODY_SIZE} bytes after decoding>"
912 )),
913 "encoding {encoding}"
914 );
915 }
916 }
917
918 fn two_member_gzip() -> Vec<u8> {
919 let mut body = gzip_compress(b"first-member-line\n");
920 body.extend(gzip_compress(b"second-member-line\n"));
921 body
922 }
923
924 #[tokio::test]
925 async fn test_format_request_multi_member_gzip_body() {
926 let body = two_member_gzip();
927 let mut request = format!(
928 "POST /upload HTTP/1.1\r\nHost: x\r\nContent-Type: text/plain\r\nContent-Encoding: \
929 gzip\r\nContent-Length: {}\r\n\r\n",
930 body.len()
931 )
932 .into_bytes();
933 request.extend_from_slice(&body);
934
935 let LogMessage::Request(text) =
936 MessageFormatter::format_request(&Bytes::from(request), "t1", Utc::now())
937 .await
938 .unwrap()
939 else {
940 panic!("not a request")
941 };
942
943 assert!(
944 text.contains("first-member-line\nsecond-member-line"),
945 "{text}"
946 );
947 }
948
949 #[test]
950 fn test_format_preformatted_response_multi_member_gzip() {
951 let mut response =
952 b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Encoding: gzip\r\n\r\n"
953 .to_vec();
954 response.extend(two_member_gzip());
955
956 let formatted = MessageFormatter::format_preformatted_response(
957 "gz2",
958 Utc::now(),
959 1,
960 &Bytes::from(response),
961 );
962
963 assert!(
964 formatted.contains("first-member-line\nsecond-member-line"),
965 "got:\n{formatted}"
966 );
967 }
968
969 #[test]
970 fn test_format_preformatted_response_gzip_with_trailing_bytes() {
971 let cut_member = gzip_compress(b"cut-member-line\n");
972 for tail in [
973 &b"\r\nHTTP/1.1 200 OK\r\n"[..],
974 &b"\x1f\x8b"[..],
975 &cut_member[..cut_member.len() - 4],
976 ] {
977 let mut response =
978 b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Encoding: gzip\r\n\r\n"
979 .to_vec();
980 response.extend(gzip_compress(b"only-member-line\n"));
981 response.extend_from_slice(tail);
982
983 let formatted = MessageFormatter::format_preformatted_response(
984 "gz3",
985 Utc::now(),
986 1,
987 &Bytes::from(response),
988 );
989
990 assert!(
991 formatted.contains("only-member-line"),
992 "tail {tail:?} got:\n{formatted}"
993 );
994 }
995 }
996
997 #[tokio::test]
998 async fn test_dechunk_body() {
999 let chunked_body = b"4\r\ntest\r\n5\r\nchunk\r\n0\r\n\r\n";
1000
1001 let result = MessageFormatter::dechunk_body(chunked_body);
1002 assert_eq!(result, b"testchunk");
1003 }
1004
1005 #[tokio::test]
1006 async fn test_dechunk_body_with_extension() {
1007 let chunked_body = b"4;extension=value\r\ntest\r\n0\r\n\r\n";
1008
1009 let result = MessageFormatter::dechunk_body(chunked_body);
1010 assert_eq!(result, b"test");
1011 }
1012
1013 #[tokio::test]
1014 async fn test_dechunk_body_incomplete() {
1015 let chunked_body = b"4\r\ntest\r\n5\r\nchun";
1016
1017 let result = MessageFormatter::dechunk_body(chunked_body);
1018 assert_eq!(result, b"testchun");
1019 }
1020
1021 #[tokio::test]
1022 async fn test_dechunk_body_invalid_size() {
1023 let chunked_body = b"XYZ\r\ntest\r\n";
1024
1025 let result = MessageFormatter::dechunk_body(chunked_body);
1026 assert_eq!(result, Vec::<u8>::new());
1027 }
1028
1029 #[test]
1030 fn test_find_headers_end() {
1031 let headers = b"Header1: Value1\r\nHeader2: Value2\r\n\r\nBody";
1032
1033 let pos = MessageFormatter::find_headers_end(headers);
1034 println!("CRLF CRLF position: {pos:?}");
1035
1036 assert_eq!(pos, Some(36));
1037
1038 let headers = b"Header1: Value1\nHeader2: Value2\n\nBody";
1039
1040 let pos = MessageFormatter::find_headers_end(headers);
1041 println!("LF LF position: {pos:?}");
1042
1043 assert_eq!(pos, Some(33));
1044
1045 let headers = b"Header1: Value1\nHeader2: Value2\nBody";
1046 assert_eq!(MessageFormatter::find_headers_end(headers), None);
1047 }
1048
1049 #[test]
1050 fn test_create_metadata_header() {
1051 let trace_id = "abc123";
1052 let timestamp = Utc::now();
1053
1054 let request_header = MessageFormatter::create_metadata_header(trace_id, timestamp, None);
1055 assert!(request_header.contains("# Trace ID: abc123"));
1056 assert!(request_header.contains("# Request at:"));
1057 assert!(!request_header.contains("# Took:"));
1058
1059 let took = 150;
1060 let response_header =
1061 MessageFormatter::create_metadata_header(trace_id, timestamp, Some(took));
1062 assert!(response_header.contains("# Trace ID: abc123"));
1063 assert!(response_header.contains("# Response at:"));
1064 assert!(response_header.contains("# Took: 150 ms"));
1065 }
1066
1067 #[test]
1068 fn test_append_headers() {
1069 let mut log_entry = String::new();
1070
1071 let header1_name = "Content-Type";
1072 let header1_value = b"application/json";
1073 let header2_name = "Content-Length";
1074 let header2_value = b"42";
1075
1076 let headers = vec![
1077 Header {
1078 name: header1_name,
1079 value: header1_value,
1080 },
1081 Header {
1082 name: header2_name,
1083 value: header2_value,
1084 },
1085 ];
1086
1087 MessageFormatter::append_headers(&headers, &mut log_entry);
1088
1089 assert!(log_entry.contains("Content-Type: application/json\n"));
1090 assert!(log_entry.contains("Content-Length: 42\n"));
1091 }
1092
1093 #[test]
1094 fn test_status_text() {
1095 assert_eq!(MessageFormatter::status_text(200), "OK");
1096 assert_eq!(MessageFormatter::status_text(404), "Not Found");
1097 assert_eq!(MessageFormatter::status_text(500), "Internal Server Error");
1098 assert_eq!(MessageFormatter::status_text(999), "Unknown Status");
1099 }
1100
1101 #[test]
1102 fn test_extract_content_type() {
1103 let header1_name = "Content-Type";
1104 let header1_value = b"application/json";
1105 let header2_name = "Content-Length";
1106 let header2_value = b"42";
1107
1108 let headers = vec![
1109 Header {
1110 name: header1_name,
1111 value: header1_value,
1112 },
1113 Header {
1114 name: header2_name,
1115 value: header2_value,
1116 },
1117 ];
1118
1119 let content_type = MessageFormatter::extract_content_type(&headers);
1120 assert_eq!(content_type, Some("application/json"));
1121
1122 let headers_no_ct = vec![Header {
1123 name: "Content-Length",
1124 value: b"42",
1125 }];
1126
1127 let content_type = MessageFormatter::extract_content_type(&headers_no_ct);
1128 assert_eq!(content_type, None);
1129 }
1130
1131 #[tokio::test]
1132 async fn test_format_request() {
1133 let request = b"GET /api/test HTTP/1.1\r\nHost: example.com\r\nContent-Type: application/json\r\n\r\n{\"test\": true}";
1134 let trace_id = "req123";
1135 let timestamp = Utc::now();
1136
1137 let result =
1138 MessageFormatter::format_request(&Bytes::from(&request[..]), trace_id, timestamp)
1139 .await
1140 .unwrap();
1141
1142 if let LogMessage::Request(formatted) = result {
1143 assert!(
1144 formatted.contains("# Trace ID: req123"),
1145 "Should contain trace ID"
1146 );
1147 assert!(
1148 formatted.contains("GET /api/test HTTP/1.1"),
1149 "Should contain request line"
1150 );
1151 assert!(
1152 formatted.contains("Host: example.com"),
1153 "Should contain host header"
1154 );
1155 assert!(
1156 formatted.contains("Content-Type: application/json"),
1157 "Should contain content-type header"
1158 );
1159
1160 assert!(formatted.contains("test"), "Should contain JSON key");
1161 assert!(formatted.contains("true"), "Should contain JSON value");
1162
1163 println!("Formatted request: {formatted}");
1164 } else {
1165 panic!("Expected Request LogMessage");
1166 }
1167 }
1168
1169 #[tokio::test]
1170 async fn test_format_response() {
1171 let response = b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 16\r\n\r\n{\"status\":\"ok\"}";
1172 let trace_id = "resp123";
1173 let timestamp = Utc::now();
1174 let took = 100;
1175
1176 let result = MessageFormatter::format_response(
1177 &Bytes::from(&response[..]),
1178 trace_id,
1179 timestamp,
1180 took,
1181 )
1182 .await
1183 .unwrap();
1184
1185 if let LogMessage::Response(formatted) = result {
1186 assert!(formatted.contains("# Trace ID: resp123"));
1187 assert!(formatted.contains("# Took: 100 ms"));
1188 assert!(formatted.contains("HTTP/1.1 200 OK"));
1189 assert!(formatted.contains("Content-Type: application/json"));
1190 assert!(formatted.contains("{\n \"status\": \"ok\"\n}"));
1191 } else {
1192 panic!("Expected Response LogMessage");
1193 }
1194 }
1195
1196 #[test]
1197 fn test_format_preformatted_response() {
1198 let response =
1199 b"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nHello";
1200 let trace_id = "pre123";
1201 let timestamp = Utc::now();
1202 let took = 50;
1203
1204 let formatted = MessageFormatter::format_preformatted_response(
1205 trace_id,
1206 timestamp,
1207 took,
1208 &Bytes::from(&response[..]),
1209 );
1210
1211 assert!(formatted.contains("# Trace ID: pre123"));
1212 assert!(formatted.contains("# Took: 50 ms"));
1213 assert!(formatted.contains("HTTP/1.1 200 OK"));
1214 assert!(formatted.contains("Content-Type: text/plain"));
1215 assert!(formatted.contains("Hello"));
1216 }
1217
1218 #[test]
1219 fn test_format_preformatted_response_gzipped() {
1220 let response = b"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Encoding: gzip\r\n\r\n\x1f\x8b";
1221 let trace_id = "gzip123";
1222 let timestamp = Utc::now();
1223 let took = 75;
1224
1225 let formatted = MessageFormatter::format_preformatted_response(
1226 trace_id,
1227 timestamp,
1228 took,
1229 &Bytes::from(&response[..]),
1230 );
1231
1232 assert!(formatted.contains("# Trace ID: gzip123"));
1233 assert!(formatted.contains("Content-Type: application/json"));
1234 assert!(formatted.contains("Content-Encoding: gzip"));
1235 }
1236
1237 fn brotli_compress(data: &[u8]) -> Vec<u8> {
1238 let mut encoder = brotli::CompressorReader::new(data, 4096, 11, 22);
1239 let mut compressed = Vec::new();
1240 std::io::copy(&mut encoder, &mut compressed).unwrap();
1241 compressed
1242 }
1243
1244 fn format_json_response(encoding_headers: &str, body: &[u8]) -> String {
1245 let mut response = format!(
1246 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n{encoding_headers}Content-Length: {}\r\n\r\n",
1247 body.len()
1248 )
1249 .into_bytes();
1250 response.extend_from_slice(body);
1251
1252 MessageFormatter::format_preformatted_response(
1253 "enc123",
1254 Utc::now(),
1255 10,
1256 &Bytes::from(response),
1257 )
1258 }
1259
1260 fn repeated_marker_json() -> String {
1261 format!("[{}]", vec![r#"{"marker":"decoded-body"}"#; 50].join(","))
1262 }
1263
1264 fn gzip_compress(data: &[u8]) -> Vec<u8> {
1265 let mut gzip = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
1266 std::io::Write::write_all(&mut gzip, data).unwrap();
1267 gzip.finish().unwrap()
1268 }
1269
1270 #[test]
1271 fn test_format_preformatted_response_brotli() {
1272 let compressed = brotli_compress(repeated_marker_json().as_bytes());
1273
1274 let formatted = format_json_response("Content-Encoding: br\r\n", &compressed);
1275
1276 assert_eq!(
1277 formatted.matches("decoded-body").count(),
1278 50,
1279 "got:\n{formatted}"
1280 );
1281 }
1282
1283 #[test]
1284 fn test_format_preformatted_response_deflate() {
1285 let body = repeated_marker_json();
1286 let mut zlib = flate2::write::ZlibEncoder::new(Vec::new(), flate2::Compression::default());
1287 std::io::Write::write_all(&mut zlib, body.as_bytes()).unwrap();
1288 let mut raw =
1289 flate2::write::DeflateEncoder::new(Vec::new(), flate2::Compression::default());
1290 std::io::Write::write_all(&mut raw, body.as_bytes()).unwrap();
1291
1292 for compressed in [zlib.finish().unwrap(), raw.finish().unwrap()] {
1293 let formatted = format_json_response("Content-Encoding: deflate\r\n", &compressed);
1294
1295 assert_eq!(
1296 formatted.matches("decoded-body").count(),
1297 50,
1298 "got:\n{formatted}"
1299 );
1300 }
1301 }
1302
1303 #[test]
1304 fn test_format_preformatted_response_gzip_then_brotli() {
1305 let compressed = brotli_compress(&gzip_compress(repeated_marker_json().as_bytes()));
1306
1307 for encoding_headers in [
1308 "Content-Encoding: gzip, br\r\n",
1309 "Content-Encoding: gzip\r\nContent-Encoding: br\r\n",
1310 ] {
1311 let formatted = format_json_response(encoding_headers, &compressed);
1312
1313 assert_eq!(
1314 formatted.matches("decoded-body").count(),
1315 50,
1316 "headers {encoding_headers:?} got:\n{formatted}"
1317 );
1318 }
1319 }
1320
1321 #[test]
1322 fn test_format_preformatted_response_caps_decoded_body() {
1323 let kept = "a".repeat(MAX_DECODED_BODY_SIZE);
1324 let payload = format!("{kept}a");
1325
1326 for (encoding_headers, compressed) in [
1327 (
1328 "Content-Encoding: br\r\n",
1329 brotli_compress(payload.as_bytes()),
1330 ),
1331 (
1332 "Content-Encoding: gzip\r\n",
1333 gzip_compress(payload.as_bytes()),
1334 ),
1335 ] {
1336 let formatted = format_json_response(encoding_headers, &compressed);
1337
1338 assert!(formatted.contains(&kept), "headers {encoding_headers:?}");
1339 assert!(
1340 !formatted.contains(&payload),
1341 "headers {encoding_headers:?}"
1342 );
1343 assert!(
1344 formatted.contains(&format!(
1345 "# <body truncated at {MAX_DECODED_BODY_SIZE} bytes after decoding>"
1346 )),
1347 "headers {encoding_headers:?}"
1348 );
1349 }
1350 }
1351
1352 #[test]
1353 fn test_process_response_body() {
1354 let headers = vec![Header {
1355 name: "Transfer-Encoding",
1356 value: b"chunked",
1357 }];
1358
1359 let chunked_body = b"4\r\ntest\r\n5\r\nchunk\r\n0\r\n\r\n";
1360
1361 let processed = MessageFormatter::process_response_body(chunked_body, &headers);
1362
1363 assert_eq!(processed, b"testchunk");
1364
1365 let normal_body = b"normal body";
1366 let headers_normal = vec![Header {
1367 name: "Content-Length",
1368 value: b"11",
1369 }];
1370
1371 let processed_normal =
1372 MessageFormatter::process_response_body(normal_body, &headers_normal);
1373 assert_eq!(processed_normal, normal_body);
1374 }
1375
1376 #[tokio::test]
1377 async fn test_handle_decompression_error() {
1378 let mut log_entry = String::new();
1379 let body = b"plain text body";
1380 let headers = vec![Header {
1381 name: "Content-Type",
1382 value: b"text/plain",
1383 }];
1384
1385 let error = anyhow::anyhow!("Test decompression error");
1386
1387 let result =
1388 MessageFormatter::handle_decompression_error(body, &headers, &mut log_entry, error)
1389 .unwrap();
1390
1391 assert!(log_entry.contains("Failed to process content: Test decompression error"));
1392
1393 assert!(
1394 log_entry.contains("plain text body"),
1395 "Log entry should contain the body text"
1396 );
1397 assert_eq!(
1398 result,
1399 Vec::<u8>::new(),
1400 "Result should be empty vec for text content"
1401 );
1402
1403 let mut js_log_entry = String::new();
1404 let js_headers = vec![Header {
1405 name: "Content-Type",
1406 value: b"application/javascript",
1407 }];
1408
1409 let js_result = MessageFormatter::handle_decompression_error(
1410 b"console.log('test')",
1411 &js_headers,
1412 &mut js_log_entry,
1413 anyhow::anyhow!("JS error"),
1414 )
1415 .unwrap();
1416
1417 assert!(js_log_entry.contains("JavaScript content could not be fully decompressed"));
1418 assert!(js_result.is_empty());
1419
1420 let mut bin_log_entry = String::new();
1421 let bin_headers = vec![Header {
1422 name: "Content-Type",
1423 value: b"application/octet-stream",
1424 }];
1425
1426 let binary_body = &[0xFF, 0xFE, 0xFD];
1427 let bin_result = MessageFormatter::handle_decompression_error(
1428 binary_body,
1429 &bin_headers,
1430 &mut bin_log_entry,
1431 anyhow::anyhow!("Binary error"),
1432 )
1433 .unwrap();
1434
1435 assert!(bin_log_entry.contains("Failed to process content: Binary error"));
1436 assert_eq!(bin_result, binary_body);
1437 }
1438
1439 #[tokio::test]
1440 async fn test_try_decompress_body() {
1441 let body = b"plain body";
1442 let headers = vec![Header {
1443 name: "Content-Type",
1444 value: b"text/plain",
1445 }];
1446
1447 let result = MessageFormatter::try_decompress_body(body, &headers)
1448 .await
1449 .unwrap();
1450 assert_eq!(result, (body.to_vec(), false));
1451
1452 let identity_headers = vec![Header {
1453 name: "Content-Encoding",
1454 value: b"identity",
1455 }];
1456
1457 let identity_result = MessageFormatter::try_decompress_body(body, &identity_headers)
1458 .await
1459 .unwrap();
1460 assert_eq!(identity_result, (body.to_vec(), false));
1461
1462 let unsupported_headers = vec![Header {
1463 name: "Content-Encoding",
1464 value: b"unknown-encoding",
1465 }];
1466
1467 let unsupported_result = MessageFormatter::try_decompress_body(body, &unsupported_headers)
1468 .await
1469 .unwrap();
1470 assert_eq!(unsupported_result, (body.to_vec(), false));
1471 }
1472}