Skip to main content

kftray_http_logs/
formatter.rs

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(&timestamp.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(&timestamp.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(&current_data).await,
537                "br" => Self::try_decompress_brotli(&current_data).await,
538                "deflate" => Self::try_decompress_deflate(&current_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}