#![cfg(feature = "compression")]
use fandhe_backend_core::interceptor::Interceptor;
use fandhe_backend_core::streaming::StreamingResponse;
use fandhe_backend_core::{Handler, Server, handle_connection};
use fandhe_backend_http::request::RequestHead;
use fandhe_backend_http::response::Response;
use fandhe_backend_plugin_compression::CompressionConfig;
use fandhe_backend_routes::Router;
use std::io::Read;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
fn build_router() -> Router {
Router::new()
.route("GET", "/large", |_head, _body| {
let body = "x".repeat(2048);
Response::new(200, body.into_bytes()).with_content_type("text/plain")
})
.route("GET", "/small", |_head, _body| {
Response::new(200, b"ok".to_vec()).with_content_type("text/plain")
})
.route("GET", "/image", |_head, _body| {
let body = vec![0u8; 2048];
Response::new(200, body).with_content_type("image/png")
})
}
async fn roundtrip_raw(server: &Server, request: &[u8]) -> Vec<u8> {
let (mut client, server_stream) = tokio::io::duplex(65536);
client.write_all(request).await.unwrap();
client.shutdown().await.unwrap();
handle_connection(server, server_stream).await;
let mut out = Vec::new();
client.read_to_end(&mut out).await.unwrap();
out
}
fn split_response(raw: &[u8]) -> (String, Vec<u8>) {
let sep = b"\r\n\r\n";
let pos = raw
.windows(sep.len())
.position(|w| w == sep)
.expect("レスポンスに空行区切りがない");
let head = String::from_utf8(raw[..pos].to_vec()).unwrap();
let body = raw[pos + sep.len()..].to_vec();
(head, body)
}
#[tokio::test]
async fn registered_with_accept_encoding_compresses_and_roundtrips() {
let config = CompressionConfig::builder().min_size(1).build();
let router = build_router();
let server = Server::new().handler(router).compression(config);
let request = b"GET /large HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(head.contains("Content-Encoding: gzip\r\n"));
assert!(head.contains("Vary: Accept-Encoding\r\n"));
let mut decoder = flate2::read::GzDecoder::new(body.as_slice());
let mut decoded = String::new();
decoder.read_to_string(&mut decoded).unwrap();
assert_eq!(decoded, "x".repeat(2048));
}
#[tokio::test]
async fn unregistered_leaves_response_unmodified() {
let router = build_router();
let server = Server::new().handler(router);
let request = b"GET /large HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(!head.contains("Content-Encoding"));
assert_eq!(body, "x".repeat(2048).into_bytes());
}
#[tokio::test]
async fn without_accept_encoding_header_stays_uncompressed() {
let config = CompressionConfig::builder().min_size(1).build();
let router = build_router();
let server = Server::new().handler(router).compression(config);
let request = b"GET /large HTTP/1.1\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(!head.contains("Content-Encoding"));
assert!(head.contains("Vary: Accept-Encoding\r\n"));
assert_eq!(body, "x".repeat(2048).into_bytes());
}
#[tokio::test]
async fn below_threshold_stays_uncompressed() {
let config = CompressionConfig::builder().build(); let router = build_router();
let server = Server::new().handler(router).compression(config);
let request = b"GET /small HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(!head.contains("Content-Encoding"));
assert_eq!(body, b"ok");
}
#[tokio::test]
async fn non_matching_content_type_stays_uncompressed() {
let config = CompressionConfig::builder().min_size(1).build();
let router = build_router();
let server = Server::new().handler(router).compression(config);
let request = b"GET /image HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(!head.contains("Content-Encoding"));
assert!(!head.contains("Vary"));
assert_eq!(body, vec![0u8; 2048]);
}
#[cfg(feature = "cors")]
#[tokio::test]
async fn cors_and_compression_apply_in_sequence() {
use fandhe_backend_plugin_cors::CorsConfig;
let cors_config = CorsConfig::builder()
.allow_origin("https://app.example.com")
.build()
.unwrap();
let compression_config = CompressionConfig::builder().min_size(1).build();
let router = build_router();
let server = Server::new()
.handler(router)
.cors(cors_config)
.compression(compression_config);
let request = b"GET /large HTTP/1.1\r\nOrigin: https://app.example.com\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(head.contains("Access-Control-Allow-Origin: https://app.example.com\r\n"));
assert!(head.contains("Content-Encoding: gzip\r\n"));
assert!(head.contains("Vary: Origin\r\n"), "head: {head}");
assert!(head.contains("Vary: Accept-Encoding\r\n"), "head: {head}");
let mut decoder = flate2::read::GzDecoder::new(body.as_slice());
let mut decoded = String::new();
decoder.read_to_string(&mut decoded).unwrap();
assert_eq!(decoded, "x".repeat(2048));
}
#[tokio::test]
async fn blocking_threshold_offload_roundtrips_same_as_inline() {
let config = CompressionConfig::builder()
.min_size(1)
.blocking_threshold(1)
.build();
let router = build_router();
let server = Server::new().handler(router).compression(config);
let request = b"GET /large HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(head.contains("Content-Encoding: gzip\r\n"));
assert!(head.contains("Vary: Accept-Encoding\r\n"));
let mut decoder = flate2::read::GzDecoder::new(body.as_slice());
let mut decoded = String::new();
decoder.read_to_string(&mut decoded).unwrap();
assert_eq!(decoded, "x".repeat(2048));
}
#[tokio::test]
async fn blocking_threshold_max_keeps_inline_path_below_threshold() {
let config = CompressionConfig::builder()
.min_size(1)
.blocking_threshold(usize::MAX)
.build();
let router = build_router();
let server = Server::new().handler(router).compression(config);
let request = b"GET /large HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(head.contains("Content-Encoding: gzip\r\n"));
let mut decoder = flate2::read::GzDecoder::new(body.as_slice());
let mut decoded = String::new();
decoder.read_to_string(&mut decoded).unwrap();
assert_eq!(decoded, "x".repeat(2048));
}
#[tokio::test]
async fn blocking_threshold_offload_falls_back_when_compression_not_beneficial() {
let config = CompressionConfig::builder()
.min_size(1)
.blocking_threshold(1)
.build();
let router = Router::new().route("GET", "/tiny", |_head, _body| {
Response::new(200, b"a".to_vec()).with_content_type("text/plain")
});
let server = Server::new().handler(router).compression(config);
let request = b"GET /tiny HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(!head.contains("Content-Encoding"));
assert_eq!(body, b"a");
}
#[tokio::test]
async fn config_built_via_core_reexport_compresses_and_roundtrips() {
let config = fandhe_backend_core::plugin_compression::CompressionConfig::builder()
.min_size(1)
.build();
let router = build_router();
let server = Server::new().handler(router).compression(config);
let request = b"GET /large HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(head.contains("Content-Encoding: gzip\r\n"));
let mut decoder = flate2::read::GzDecoder::new(body.as_slice());
let mut decoded = String::new();
decoder.read_to_string(&mut decoded).unwrap();
assert_eq!(decoded, "x".repeat(2048));
}
struct ExpandBody;
impl Interceptor for ExpandBody {
fn name(&self) -> &'static str {
"expand-body"
}
fn map_response(&self, _head: &RequestHead, response: Response) -> Response {
Response::new(response.status, "y".repeat(2048).into_bytes())
.with_content_type("text/plain")
}
}
#[tokio::test]
async fn interceptor_map_response_output_is_compressed() {
let config = CompressionConfig::builder().min_size(1).build();
let router = build_router();
let server = Server::new()
.interceptor(ExpandBody)
.handler(router)
.compression(config);
let request = b"GET /small HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"));
assert!(head.contains("Content-Encoding: gzip\r\n"));
let mut decoder = flate2::read::GzDecoder::new(body.as_slice());
let mut decoded = String::new();
decoder.read_to_string(&mut decoded).unwrap();
assert_eq!(decoded, "y".repeat(2048));
}
struct StreamingOkHandler;
impl Handler for StreamingOkHandler {
fn handle(&self, _head: &RequestHead, _body: &[u8]) -> fandhe_backend_routes::HandlerFuture {
Box::pin(std::future::ready(Response::empty(599)))
}
fn handle_streaming(&self, _head: &RequestHead, _body: &[u8]) -> Option<StreamingResponse> {
let (response, writer) = StreamingResponse::channel(200, Some("text/plain"), 4);
tokio::spawn(async move {
let _ = writer.send("x".repeat(2048).into_bytes()).await;
let _ = writer.finish().await;
});
Some(response)
}
}
struct StreamingMultiChunkHandler;
impl Handler for StreamingMultiChunkHandler {
fn handle(&self, _head: &RequestHead, _body: &[u8]) -> fandhe_backend_routes::HandlerFuture {
Box::pin(std::future::ready(Response::empty(599)))
}
fn handle_streaming(&self, _head: &RequestHead, _body: &[u8]) -> Option<StreamingResponse> {
let (response, writer) = StreamingResponse::channel(200, Some("text/event-stream"), 4);
tokio::spawn(async move {
for chunk in ["event: a\ndata: ".repeat(50), "event: b\ndata: ".repeat(50)] {
let _ = writer.send(chunk.into_bytes()).await;
}
let _ = writer.finish().await;
});
Some(response)
}
}
fn dechunk(body: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut rest = body;
loop {
let line_end = rest
.windows(2)
.position(|w| w == b"\r\n")
.expect("チャンクサイズ行の終端がない");
let size_line = std::str::from_utf8(&rest[..line_end]).unwrap();
let size = usize::from_str_radix(size_line.trim(), 16).unwrap();
rest = &rest[line_end + 2..];
if size == 0 {
break;
}
out.extend_from_slice(&rest[..size]);
rest = &rest[size + 2..]; }
out
}
fn dechunk_lenient(body: &[u8]) -> Vec<u8> {
let mut out = Vec::new();
let mut rest = body;
while let Some(line_end) = rest.windows(2).position(|w| w == b"\r\n") {
let Ok(size_line) = std::str::from_utf8(&rest[..line_end]) else {
break;
};
let Ok(size) = usize::from_str_radix(size_line.trim(), 16) else {
break;
};
rest = &rest[line_end + 2..];
if size == 0 {
break;
}
if rest.len() < size + 2 {
out.extend_from_slice(&rest[..rest.len().min(size)]);
break;
}
out.extend_from_slice(&rest[..size]);
rest = &rest[size + 2..];
}
out
}
#[tokio::test]
async fn streaming_response_not_compressed_by_default() {
let config = CompressionConfig::builder().min_size(1).build();
let server = Server::new()
.handler(StreamingOkHandler)
.compression(config);
let request = b"GET / HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"), "head: {head}");
assert!(!head.contains("Content-Encoding"), "head: {head}");
assert!(
head.contains("Transfer-Encoding: chunked\r\n"),
"head: {head}"
);
let expected_chunk_size = format!("{:x}", "x".repeat(2048).len());
let body_text = String::from_utf8_lossy(&body);
assert!(
body_text.starts_with(&format!("{expected_chunk_size}\r\n")),
"body: {body_text}"
);
assert!(body_text.contains(&"x".repeat(2048)), "body: {body_text}");
assert!(body_text.ends_with("0\r\n\r\n"), "body: {body_text}");
}
#[tokio::test]
async fn streaming_response_unregistered_compression_stays_identity() {
let server = Server::new().handler(StreamingOkHandler);
let request = b"GET / HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"), "head: {head}");
assert!(!head.contains("Content-Encoding"), "head: {head}");
assert!(!head.contains("Vary"), "head: {head}");
let raw_body = dechunk(&body);
assert_eq!(raw_body, "x".repeat(2048).into_bytes());
}
#[tokio::test]
async fn streaming_response_compress_streaming_enabled_roundtrips_multi_chunk() {
let config = CompressionConfig::builder()
.min_size(1)
.compress_streaming(true)
.build();
let server = Server::new()
.handler(StreamingMultiChunkHandler)
.compression(config);
let request = b"GET / HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"), "head: {head}");
assert!(head.contains("Content-Encoding: gzip\r\n"), "head: {head}");
assert!(head.contains("Vary: Accept-Encoding\r\n"), "head: {head}");
assert!(
head.contains("Transfer-Encoding: chunked\r\n"),
"head: {head}"
);
let compressed = dechunk(&body);
let mut decoder = flate2::read::GzDecoder::new(compressed.as_slice());
let mut decoded = String::new();
decoder.read_to_string(&mut decoded).unwrap();
let expected = "event: a\ndata: ".repeat(50) + &"event: b\ndata: ".repeat(50);
assert_eq!(decoded, expected);
}
#[cfg(feature = "cors")]
#[tokio::test]
async fn streaming_response_cors_and_compress_streaming_both_apply_vary_tokens() {
use fandhe_backend_plugin_cors::CorsConfig;
let cors_config = CorsConfig::builder()
.allow_origin("https://app.example.com")
.build()
.unwrap();
let compression_config = CompressionConfig::builder()
.min_size(1)
.compress_streaming(true)
.build();
let server = Server::new()
.handler(StreamingMultiChunkHandler)
.cors(cors_config)
.compression(compression_config);
let request = b"GET / HTTP/1.1\r\nOrigin: https://app.example.com\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"), "head: {head}");
assert!(
head.contains("Access-Control-Allow-Origin: https://app.example.com\r\n"),
"head: {head}"
);
assert!(head.contains("Content-Encoding: gzip\r\n"), "head: {head}");
assert!(head.contains("Vary: Origin\r\n"), "head: {head}");
assert!(head.contains("Vary: Accept-Encoding\r\n"), "head: {head}");
let compressed = dechunk(&body);
let mut decoder = flate2::read::GzDecoder::new(compressed.as_slice());
let mut decoded = String::new();
decoder.read_to_string(&mut decoded).unwrap();
let expected = "event: a\ndata: ".repeat(50) + &"event: b\ndata: ".repeat(50);
assert_eq!(decoded, expected);
}
#[tokio::test]
async fn streaming_response_compress_streaming_enabled_skips_without_accept_encoding() {
let config = CompressionConfig::builder()
.min_size(1)
.compress_streaming(true)
.build();
let server = Server::new()
.handler(StreamingOkHandler)
.compression(config);
let request = b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"), "head: {head}");
assert!(!head.contains("Content-Encoding"), "head: {head}");
assert!(head.contains("Vary: Accept-Encoding\r\n"), "head: {head}");
let raw_body = dechunk(&body);
assert_eq!(raw_body, "x".repeat(2048).into_bytes());
}
#[tokio::test]
async fn streaming_response_compress_streaming_enabled_skips_non_matching_content_type() {
let config = CompressionConfig::builder()
.min_size(1)
.compress_streaming(true)
.build();
struct StreamingImageHandler;
impl Handler for StreamingImageHandler {
fn handle(
&self,
_head: &RequestHead,
_body: &[u8],
) -> fandhe_backend_routes::HandlerFuture {
Box::pin(std::future::ready(Response::empty(599)))
}
fn handle_streaming(&self, _head: &RequestHead, _body: &[u8]) -> Option<StreamingResponse> {
let (response, writer) = StreamingResponse::channel(200, Some("image/png"), 4);
tokio::spawn(async move {
let _ = writer.send(vec![0u8; 2048]).await;
let _ = writer.finish().await;
});
Some(response)
}
}
let server = Server::new()
.handler(StreamingImageHandler)
.compression(config);
let request = b"GET / HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(!head.contains("Content-Encoding"), "head: {head}");
assert!(!head.contains("Vary"), "head: {head}");
let raw_body = dechunk(&body);
assert_eq!(raw_body, vec![0u8; 2048]);
}
#[tokio::test]
async fn streaming_response_compress_streaming_enabled_aborted_closes_without_trailer() {
struct AbortingHandler;
impl Handler for AbortingHandler {
fn handle(
&self,
_head: &RequestHead,
_body: &[u8],
) -> fandhe_backend_routes::HandlerFuture {
Box::pin(std::future::ready(Response::empty(599)))
}
fn handle_streaming(&self, _head: &RequestHead, _body: &[u8]) -> Option<StreamingResponse> {
let (response, writer) = StreamingResponse::channel(200, Some("text/plain"), 4);
tokio::spawn(async move {
let _ = writer.send("partial-data".repeat(100).into_bytes()).await;
});
Some(response)
}
}
let config = CompressionConfig::builder()
.min_size(1)
.compress_streaming(true)
.build();
let server = Server::new().handler(AbortingHandler).compression(config);
let request = b"GET / HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"), "head: {head}");
assert!(head.contains("Content-Encoding: gzip\r\n"), "head: {head}");
assert!(!body.ends_with(b"0\r\n\r\n"), "body: {body:?}");
let compressed = dechunk_lenient(&body);
assert!(!compressed.is_empty(), "圧縮済みデータが届いていない");
let mut decoder = flate2::read::GzDecoder::new(compressed.as_slice());
let mut decoded = String::new();
let result = decoder.read_to_string(&mut decoded);
assert!(
result.is_err(),
"trailer 欠如の不完全な gzip ストリームのはずが正常終了した: {decoded:?}"
);
assert!(
"partial-data".repeat(100).starts_with(&decoded),
"decoded: {decoded:?}"
);
}
#[tokio::test]
async fn streaming_response_identity_aborted_closes_without_terminator() {
struct AbortingHandler;
impl Handler for AbortingHandler {
fn handle(
&self,
_head: &RequestHead,
_body: &[u8],
) -> fandhe_backend_routes::HandlerFuture {
Box::pin(std::future::ready(Response::empty(599)))
}
fn handle_streaming(&self, _head: &RequestHead, _body: &[u8]) -> Option<StreamingResponse> {
let (response, writer) = StreamingResponse::channel(200, Some("text/plain"), 4);
tokio::spawn(async move {
let _ = writer.send("partial-data".repeat(100).into_bytes()).await;
});
Some(response)
}
}
let server = Server::new().handler(AbortingHandler);
let request = b"GET / HTTP/1.1\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"), "head: {head}");
assert!(!head.contains("Content-Encoding"), "head: {head}");
assert!(!body.ends_with(b"0\r\n\r\n"), "body: {body:?}");
let raw_body = dechunk_lenient(&body);
assert!(
"partial-data"
.repeat(100)
.starts_with(std::str::from_utf8(&raw_body).unwrap_or_default()),
"body: {raw_body:?}"
);
}
#[tokio::test]
async fn streaming_response_compress_streaming_enabled_map_response_204_skips_body() {
struct ForceNoContent;
impl Interceptor for ForceNoContent {
fn name(&self) -> &'static str {
"force-no-content"
}
fn map_response(&self, _head: &RequestHead, _response: Response) -> Response {
Response::empty(204).with_content_type("text/plain")
}
}
let config = CompressionConfig::builder()
.min_size(1)
.compress_streaming(true)
.build();
let server = Server::new()
.interceptor(ForceNoContent)
.handler(StreamingOkHandler)
.compression(config);
let request = b"GET / HTTP/1.1\r\nAccept-Encoding: gzip\r\nConnection: close\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(
head.starts_with("HTTP/1.1 204 No Content\r\n"),
"head: {head}"
);
assert!(!head.contains("Content-Encoding"), "head: {head}");
assert!(!head.contains("Transfer-Encoding"), "head: {head}");
assert!(!head.contains("Vary"), "head: {head}");
assert!(body.is_empty(), "body: {body:?}");
}
#[tokio::test]
async fn streaming_response_compress_streaming_enabled_http10_stays_uncompressed() {
let config = CompressionConfig::builder()
.min_size(1)
.compress_streaming(true)
.build();
let server = Server::new()
.handler(StreamingOkHandler)
.compression(config);
let request = b"GET / HTTP/1.0\r\nAccept-Encoding: gzip\r\n\r\n";
let raw = roundtrip_raw(&server, request).await;
let (head, body) = split_response(&raw);
assert!(head.starts_with("HTTP/1.1 200 OK\r\n"), "head: {head}");
assert!(!head.contains("Content-Encoding"), "head: {head}");
assert!(!head.contains("Transfer-Encoding"), "head: {head}");
assert_eq!(body, "x".repeat(2048).into_bytes());
}