From 35ecc0cce2af0a66261569fe70acd7a5692de3bf Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Wed, 28 Feb 2024 14:52:03 +0100 Subject: [PATCH 01/10] add example about compression --- examples/compression/Cargo.toml | 15 ++++++++++ examples/compression/README.md | 32 +++++++++++++++++++++ examples/compression/data/products.json | 12 ++++++++ examples/compression/data/products.json.gz | Bin 0 -> 99 bytes examples/compression/src/main.rs | 32 +++++++++++++++++++++ 5 files changed, 91 insertions(+) create mode 100644 examples/compression/Cargo.toml create mode 100644 examples/compression/README.md create mode 100644 examples/compression/data/products.json create mode 100644 examples/compression/data/products.json.gz create mode 100644 examples/compression/src/main.rs diff --git a/examples/compression/Cargo.toml b/examples/compression/Cargo.toml new file mode 100644 index 0000000000..fb6335e466 --- /dev/null +++ b/examples/compression/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "compression" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +axum = { path = "../../axum" } +axum-extra = { path = "../../axum-extra", features = ["typed-header"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["compression-gzip", "decompression-gzip"] } +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } +serde_json = "1" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/examples/compression/README.md b/examples/compression/README.md new file mode 100644 index 0000000000..4fc04b8d93 --- /dev/null +++ b/examples/compression/README.md @@ -0,0 +1,32 @@ +# compression + +This example shows how to: +- automatically decompress request bodies when necessary +- compress reponse bodies based on the `accept` header. + +## Running + +``` +cargo run +``` + +## Sending compressed requests + +``` +curl -v -g 'http://localhost:3000/' \ + -H "Content-Type: application/json" \ + -H "Content-Encoding: gzip" \ + --compressed \ + --data-binary @data/products.json.gz +``` + +(Notice the `Content-Encoding: gzip` in the request, and `content-encoding: gzip` in the response.) + +## Sending non compressed requests + +``` +curl -v -g 'http://localhost:3000/' \ + -H "Content-Type: application/json" \ + --compressed \ + --data-binary @data/products.json +``` diff --git a/examples/compression/data/products.json b/examples/compression/data/products.json new file mode 100644 index 0000000000..a234fbdd2a --- /dev/null +++ b/examples/compression/data/products.json @@ -0,0 +1,12 @@ +{ + "products": [ + { + "id": 1, + "name": "Product 1" + }, + { + "id": 2, + "name": "Product 2" + } + ] +} diff --git a/examples/compression/data/products.json.gz b/examples/compression/data/products.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..91d398955b5f43eac542354723b8638c34a4a6f9 GIT binary patch literal 99 zcmV-p0G$6HiwFo3EAM0g18{P0WOZY7b1rIgZ*Bmq=28FxrGldTl+xsqVkIkuXs`$f zRKr) -> Json { + Json(value) +} From b9bdb4d13644b699d4624c7e797f996aea97e108 Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Wed, 28 Feb 2024 14:56:07 +0100 Subject: [PATCH 02/10] fix typo --- examples/compression/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/compression/README.md b/examples/compression/README.md index 4fc04b8d93..fda35dac77 100644 --- a/examples/compression/README.md +++ b/examples/compression/README.md @@ -2,7 +2,7 @@ This example shows how to: - automatically decompress request bodies when necessary -- compress reponse bodies based on the `accept` header. +- compress response bodies based on the `accept` header. ## Running From e3ca95005e074850ee5c8038c4e093d36395aad1 Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Wed, 28 Feb 2024 14:57:42 +0100 Subject: [PATCH 03/10] sort dependencies --- examples/compression/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/compression/Cargo.toml b/examples/compression/Cargo.toml index fb6335e466..3de9085dee 100644 --- a/examples/compression/Cargo.toml +++ b/examples/compression/Cargo.toml @@ -7,9 +7,9 @@ publish = false [dependencies] axum = { path = "../../axum" } axum-extra = { path = "../../axum-extra", features = ["typed-header"] } +serde_json = "1" +tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tower = "0.4" tower-http = { version = "0.5", features = ["compression-gzip", "decompression-gzip"] } -tokio = { version = "1", features = ["macros", "rt-multi-thread"] } -serde_json = "1" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } From a862c2202a74b5739052cf3f731a81a2a1baaf7e Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Thu, 29 Feb 2024 17:21:04 +0100 Subject: [PATCH 04/10] test request body decompression --- examples/compression/Cargo.toml | 9 +- examples/compression/src/main.rs | 162 ++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 6 deletions(-) diff --git a/examples/compression/Cargo.toml b/examples/compression/Cargo.toml index 3de9085dee..aa8ab512fa 100644 --- a/examples/compression/Cargo.toml +++ b/examples/compression/Cargo.toml @@ -10,6 +10,13 @@ axum-extra = { path = "../../axum-extra", features = ["typed-header"] } serde_json = "1" tokio = { version = "1", features = ["macros", "rt-multi-thread"] } tower = "0.4" -tower-http = { version = "0.5", features = ["compression-gzip", "decompression-gzip"] } +tower-http = { version = "0.5", features = ["compression-full", "decompression-full"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } + +[dev-dependencies] +assert-json-diff = "2.0" +brotli = "3.4" +flate2 = "1" +http = "1" +zstd = "0.13" diff --git a/examples/compression/src/main.rs b/examples/compression/src/main.rs index 741af90e36..a02c52e37f 100644 --- a/examples/compression/src/main.rs +++ b/examples/compression/src/main.rs @@ -14,11 +14,7 @@ async fn main() { .with(tracing_subscriber::fmt::layer()) .init(); - let app: Router = Router::new().route("/", post(root)).layer( - ServiceBuilder::new() - .layer(RequestDecompressionLayer::new()) - .layer(CompressionLayer::new()), - ); + let app: Router = app(); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await @@ -27,6 +23,162 @@ async fn main() { axum::serve(listener, app).await.unwrap(); } +fn app() -> Router { + Router::new().route("/", post(root)).layer( + ServiceBuilder::new() + .layer(RequestDecompressionLayer::new()) + .layer(CompressionLayer::new()), + ) +} + async fn root(Json(value): Json) -> Json { Json(value) } + +#[cfg(test)] +mod tests { + use assert_json_diff::assert_json_eq; + use axum::{ + body::{Body, Bytes}, + response::Response, + }; + use brotli::enc::BrotliEncoderParams; + use flate2::{write::GzEncoder, Compression}; + use http::StatusCode; + use serde_json::{json, Value}; + use std::io::Write; + use tower::ServiceExt; + + use super::*; + + #[tokio::test] + async fn handle_uncompressed_request_bodies() { + // Given + + let body = serde_json::to_vec(&json()).unwrap(); + + let compressed_request = http::Request::post("/") + .header(http::header::CONTENT_TYPE, "application/json") + .body(Body::from(body)) + .unwrap(); + + // When + + let response = app().oneshot(compressed_request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + assert_json_eq!(json_from_response(response).await, json()); + } + + #[tokio::test] + async fn decompress_gzip_request_bodies() { + // Given + + let body = compress_gzip(&json()); + + let compressed_request = http::Request::post("/") + .header(http::header::CONTENT_TYPE, "application/json") + .header("Content-Encoding", "gzip") + .body(Body::from(body)) + .unwrap(); + + // When + + let response = app().oneshot(compressed_request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + assert_json_eq!(json_from_response(response).await, json()); + } + + #[tokio::test] + async fn decompress_br_request_bodies() { + // Given + + let body = compress_br(&json()); + + let compressed_request = http::Request::post("/") + .header(http::header::CONTENT_TYPE, "application/json") + .header("Content-Encoding", "br") + .body(Body::from(body)) + .unwrap(); + + // When + + let response = app().oneshot(compressed_request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + assert_json_eq!(json_from_response(response).await, json()); + } + + #[tokio::test] + async fn decompress_zstd_request_bodies() { + // Given + + let body = compress_zstd(&json()); + + let compressed_request = http::Request::post("/") + .header(http::header::CONTENT_TYPE, "application/json") + .header("Content-Encoding", "zstd") + .body(Body::from(body)) + .unwrap(); + + // When + + let response = app().oneshot(compressed_request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + assert_json_eq!(json_from_response(response).await, json()); + } + + fn json() -> Value { + json!({ + "name": "foo", + "mainProduct": { + "typeId": "product", + "id": "p1" + }, + }) + } + + async fn json_from_response(response: Response) -> Value { + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + body_as_json(body) + } + + fn body_as_json(body: Bytes) -> Value { + serde_json::from_slice(body.as_ref()).unwrap() + } + + fn compress_gzip(json: &Value) -> Vec { + let request_body = serde_json::to_vec(&json).unwrap(); + + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&request_body).unwrap(); + encoder.finish().unwrap() + } + + fn compress_br(json: &Value) -> Vec { + let request_body = serde_json::to_vec(&json).unwrap(); + let mut result = Vec::new(); + + let params = BrotliEncoderParams::default(); + let _ = brotli::enc::BrotliCompress(&mut &request_body[..], &mut result, ¶ms).unwrap(); + + result + } + + fn compress_zstd(json: &Value) -> Vec { + let request_body = serde_json::to_vec(&json).unwrap(); + zstd::stream::encode_all(std::io::Cursor::new(request_body), 4).unwrap() + } +} From 9a0a110c3b4531729d999040d4d5aa926553d05b Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Thu, 29 Feb 2024 17:46:46 +0100 Subject: [PATCH 05/10] test compression --- examples/compression/src/main.rs | 132 +++++++++++++++++++++++++++---- 1 file changed, 117 insertions(+), 15 deletions(-) diff --git a/examples/compression/src/main.rs b/examples/compression/src/main.rs index a02c52e37f..e26dcf4b29 100644 --- a/examples/compression/src/main.rs +++ b/examples/compression/src/main.rs @@ -43,10 +43,10 @@ mod tests { response::Response, }; use brotli::enc::BrotliEncoderParams; - use flate2::{write::GzEncoder, Compression}; - use http::StatusCode; + use flate2::{read::GzDecoder, write::GzEncoder, Compression}; + use http::{header, StatusCode}; use serde_json::{json, Value}; - use std::io::Write; + use std::io::{Read, Write}; use tower::ServiceExt; use super::*; @@ -55,11 +55,11 @@ mod tests { async fn handle_uncompressed_request_bodies() { // Given - let body = serde_json::to_vec(&json()).unwrap(); + let body = json(); let compressed_request = http::Request::post("/") - .header(http::header::CONTENT_TYPE, "application/json") - .body(Body::from(body)) + .header(header::CONTENT_TYPE, "application/json") + .body(json_body(&body)) .unwrap(); // When @@ -79,8 +79,8 @@ mod tests { let body = compress_gzip(&json()); let compressed_request = http::Request::post("/") - .header(http::header::CONTENT_TYPE, "application/json") - .header("Content-Encoding", "gzip") + .header(header::CONTENT_TYPE, "application/json") + .header(header::CONTENT_ENCODING, "gzip") .body(Body::from(body)) .unwrap(); @@ -101,8 +101,8 @@ mod tests { let body = compress_br(&json()); let compressed_request = http::Request::post("/") - .header(http::header::CONTENT_TYPE, "application/json") - .header("Content-Encoding", "br") + .header(header::CONTENT_TYPE, "application/json") + .header(header::CONTENT_ENCODING, "br") .body(Body::from(body)) .unwrap(); @@ -123,8 +123,8 @@ mod tests { let body = compress_zstd(&json()); let compressed_request = http::Request::post("/") - .header(http::header::CONTENT_TYPE, "application/json") - .header("Content-Encoding", "zstd") + .header(header::CONTENT_TYPE, "application/json") + .header(header::CONTENT_ENCODING, "zstd") .body(Body::from(body)) .unwrap(); @@ -138,6 +138,100 @@ mod tests { assert_json_eq!(json_from_response(response).await, json()); } + #[tokio::test] + async fn do_not_compress_response_bodies() { + // Given + let request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .body(json_body(&json())) + .unwrap(); + + // When + + let response = app().oneshot(request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + assert_json_eq!(json_from_response(response).await, json()); + } + + #[tokio::test] + async fn compress_response_bodies_with_gzip() { + // Given + let request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .header(header::ACCEPT_ENCODING, "gzip") + .body(json_body(&json())) + .unwrap(); + + // When + + let response = app().oneshot(request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + let response_body = byte_from_response(response).await; + let mut decoder = GzDecoder::new(response_body.as_ref()); + let mut decompress_body = String::new(); + decoder.read_to_string(&mut decompress_body).unwrap(); + assert_json_eq!( + serde_json::from_str::(&decompress_body).unwrap(), + json() + ); + } + + #[tokio::test] + async fn compress_response_bodies_with_br() { + // Given + let request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .header(header::ACCEPT_ENCODING, "br") + .body(json_body(&json())) + .unwrap(); + + // When + + let response = app().oneshot(request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + let response_body = byte_from_response(response).await; + let mut decompress_body = Vec::new(); + brotli::BrotliDecompress(&mut response_body.as_ref(), &mut decompress_body).unwrap(); + assert_json_eq!( + serde_json::from_slice::(&decompress_body).unwrap(), + json() + ); + } + + #[tokio::test] + async fn compress_response_bodies_with_zstd() { + // Given + let request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .header(header::ACCEPT_ENCODING, "zstd") + .body(json_body(&json())) + .unwrap(); + + // When + + let response = app().oneshot(request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + let response_body = byte_from_response(response).await; + let decompress_body = + zstd::stream::decode_all(std::io::Cursor::new(response_body)).unwrap(); + assert_json_eq!( + serde_json::from_slice::(&decompress_body).unwrap(), + json() + ); + } + fn json() -> Value { json!({ "name": "foo", @@ -148,13 +242,21 @@ mod tests { }) } + fn json_body(input: &Value) -> Body { + Body::from(serde_json::to_vec(&input).unwrap()) + } + async fn json_from_response(response: Response) -> Value { - let body = axum::body::to_bytes(response.into_body(), usize::MAX) - .await - .unwrap(); + let body = byte_from_response(response).await; body_as_json(body) } + async fn byte_from_response(response: Response) -> Bytes { + axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap() + } + fn body_as_json(body: Bytes) -> Value { serde_json::from_slice(body.as_ref()).unwrap() } From 46a6276247e2f779cc16d98304657b0a40d8d322 Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Thu, 2 May 2024 10:54:24 +0200 Subject: [PATCH 06/10] rename to 'example-compression' --- examples/compression/Cargo.toml | 2 +- examples/compression/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/compression/Cargo.toml b/examples/compression/Cargo.toml index aa8ab512fa..d5fdcf8e7a 100644 --- a/examples/compression/Cargo.toml +++ b/examples/compression/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "compression" +name = "example-compression" version = "0.1.0" edition = "2021" publish = false diff --git a/examples/compression/README.md b/examples/compression/README.md index fda35dac77..f9ced5d2bb 100644 --- a/examples/compression/README.md +++ b/examples/compression/README.md @@ -7,7 +7,7 @@ This example shows how to: ## Running ``` -cargo run +cargo run -p example-compression ``` ## Sending compressed requests From 8efe6f1b73c4c8562fd4772c02410849c8044076 Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Thu, 2 May 2024 10:55:42 +0200 Subject: [PATCH 07/10] add delimiter before tests --- examples/compression/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/compression/src/main.rs b/examples/compression/src/main.rs index e26dcf4b29..08382c39fa 100644 --- a/examples/compression/src/main.rs +++ b/examples/compression/src/main.rs @@ -35,6 +35,8 @@ async fn root(Json(value): Json) -> Json { Json(value) } +// ======================== TESTS ======================== + #[cfg(test)] mod tests { use assert_json_diff::assert_json_eq; From f26856ee8b02167ad1d22ee7706f0950f7f54a2d Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Thu, 2 May 2024 21:48:26 +0200 Subject: [PATCH 08/10] update title Co-authored-by: Jonas Platte --- examples/compression/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/compression/README.md b/examples/compression/README.md index f9ced5d2bb..3f0ed94da7 100644 --- a/examples/compression/README.md +++ b/examples/compression/README.md @@ -22,7 +22,7 @@ curl -v -g 'http://localhost:3000/' \ (Notice the `Content-Encoding: gzip` in the request, and `content-encoding: gzip` in the response.) -## Sending non compressed requests +## Sending uncompressed requests ``` curl -v -g 'http://localhost:3000/' \ From fda6f0914cdd07f83bb10179210eca1994b1212a Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Thu, 2 May 2024 21:48:41 +0200 Subject: [PATCH 09/10] fix log Co-authored-by: Jonas Platte --- examples/compression/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/compression/src/main.rs b/examples/compression/src/main.rs index 08382c39fa..14a60824dd 100644 --- a/examples/compression/src/main.rs +++ b/examples/compression/src/main.rs @@ -9,7 +9,7 @@ async fn main() { tracing_subscriber::registry() .with( tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "compression=trace".into()), + .unwrap_or_else(|_| "example-compression=trace".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); From 179fff01852679bafc7324aad242161fb1dbe651 Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Fri, 3 May 2024 06:58:45 +0200 Subject: [PATCH 10/10] move tests to separated file for more clarity --- examples/compression/src/main.rs | 255 +----------------------------- examples/compression/src/tests.rs | 245 ++++++++++++++++++++++++++++ 2 files changed, 248 insertions(+), 252 deletions(-) create mode 100644 examples/compression/src/tests.rs diff --git a/examples/compression/src/main.rs b/examples/compression/src/main.rs index 14a60824dd..1fa1bb4976 100644 --- a/examples/compression/src/main.rs +++ b/examples/compression/src/main.rs @@ -4,6 +4,9 @@ use tower::ServiceBuilder; use tower_http::{compression::CompressionLayer, decompression::RequestDecompressionLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +#[cfg(test)] +mod tests; + #[tokio::main] async fn main() { tracing_subscriber::registry() @@ -34,255 +37,3 @@ fn app() -> Router { async fn root(Json(value): Json) -> Json { Json(value) } - -// ======================== TESTS ======================== - -#[cfg(test)] -mod tests { - use assert_json_diff::assert_json_eq; - use axum::{ - body::{Body, Bytes}, - response::Response, - }; - use brotli::enc::BrotliEncoderParams; - use flate2::{read::GzDecoder, write::GzEncoder, Compression}; - use http::{header, StatusCode}; - use serde_json::{json, Value}; - use std::io::{Read, Write}; - use tower::ServiceExt; - - use super::*; - - #[tokio::test] - async fn handle_uncompressed_request_bodies() { - // Given - - let body = json(); - - let compressed_request = http::Request::post("/") - .header(header::CONTENT_TYPE, "application/json") - .body(json_body(&body)) - .unwrap(); - - // When - - let response = app().oneshot(compressed_request).await.unwrap(); - - // Then - - assert_eq!(response.status(), StatusCode::OK); - assert_json_eq!(json_from_response(response).await, json()); - } - - #[tokio::test] - async fn decompress_gzip_request_bodies() { - // Given - - let body = compress_gzip(&json()); - - let compressed_request = http::Request::post("/") - .header(header::CONTENT_TYPE, "application/json") - .header(header::CONTENT_ENCODING, "gzip") - .body(Body::from(body)) - .unwrap(); - - // When - - let response = app().oneshot(compressed_request).await.unwrap(); - - // Then - - assert_eq!(response.status(), StatusCode::OK); - assert_json_eq!(json_from_response(response).await, json()); - } - - #[tokio::test] - async fn decompress_br_request_bodies() { - // Given - - let body = compress_br(&json()); - - let compressed_request = http::Request::post("/") - .header(header::CONTENT_TYPE, "application/json") - .header(header::CONTENT_ENCODING, "br") - .body(Body::from(body)) - .unwrap(); - - // When - - let response = app().oneshot(compressed_request).await.unwrap(); - - // Then - - assert_eq!(response.status(), StatusCode::OK); - assert_json_eq!(json_from_response(response).await, json()); - } - - #[tokio::test] - async fn decompress_zstd_request_bodies() { - // Given - - let body = compress_zstd(&json()); - - let compressed_request = http::Request::post("/") - .header(header::CONTENT_TYPE, "application/json") - .header(header::CONTENT_ENCODING, "zstd") - .body(Body::from(body)) - .unwrap(); - - // When - - let response = app().oneshot(compressed_request).await.unwrap(); - - // Then - - assert_eq!(response.status(), StatusCode::OK); - assert_json_eq!(json_from_response(response).await, json()); - } - - #[tokio::test] - async fn do_not_compress_response_bodies() { - // Given - let request = http::Request::post("/") - .header(header::CONTENT_TYPE, "application/json") - .body(json_body(&json())) - .unwrap(); - - // When - - let response = app().oneshot(request).await.unwrap(); - - // Then - - assert_eq!(response.status(), StatusCode::OK); - assert_json_eq!(json_from_response(response).await, json()); - } - - #[tokio::test] - async fn compress_response_bodies_with_gzip() { - // Given - let request = http::Request::post("/") - .header(header::CONTENT_TYPE, "application/json") - .header(header::ACCEPT_ENCODING, "gzip") - .body(json_body(&json())) - .unwrap(); - - // When - - let response = app().oneshot(request).await.unwrap(); - - // Then - - assert_eq!(response.status(), StatusCode::OK); - let response_body = byte_from_response(response).await; - let mut decoder = GzDecoder::new(response_body.as_ref()); - let mut decompress_body = String::new(); - decoder.read_to_string(&mut decompress_body).unwrap(); - assert_json_eq!( - serde_json::from_str::(&decompress_body).unwrap(), - json() - ); - } - - #[tokio::test] - async fn compress_response_bodies_with_br() { - // Given - let request = http::Request::post("/") - .header(header::CONTENT_TYPE, "application/json") - .header(header::ACCEPT_ENCODING, "br") - .body(json_body(&json())) - .unwrap(); - - // When - - let response = app().oneshot(request).await.unwrap(); - - // Then - - assert_eq!(response.status(), StatusCode::OK); - let response_body = byte_from_response(response).await; - let mut decompress_body = Vec::new(); - brotli::BrotliDecompress(&mut response_body.as_ref(), &mut decompress_body).unwrap(); - assert_json_eq!( - serde_json::from_slice::(&decompress_body).unwrap(), - json() - ); - } - - #[tokio::test] - async fn compress_response_bodies_with_zstd() { - // Given - let request = http::Request::post("/") - .header(header::CONTENT_TYPE, "application/json") - .header(header::ACCEPT_ENCODING, "zstd") - .body(json_body(&json())) - .unwrap(); - - // When - - let response = app().oneshot(request).await.unwrap(); - - // Then - - assert_eq!(response.status(), StatusCode::OK); - let response_body = byte_from_response(response).await; - let decompress_body = - zstd::stream::decode_all(std::io::Cursor::new(response_body)).unwrap(); - assert_json_eq!( - serde_json::from_slice::(&decompress_body).unwrap(), - json() - ); - } - - fn json() -> Value { - json!({ - "name": "foo", - "mainProduct": { - "typeId": "product", - "id": "p1" - }, - }) - } - - fn json_body(input: &Value) -> Body { - Body::from(serde_json::to_vec(&input).unwrap()) - } - - async fn json_from_response(response: Response) -> Value { - let body = byte_from_response(response).await; - body_as_json(body) - } - - async fn byte_from_response(response: Response) -> Bytes { - axum::body::to_bytes(response.into_body(), usize::MAX) - .await - .unwrap() - } - - fn body_as_json(body: Bytes) -> Value { - serde_json::from_slice(body.as_ref()).unwrap() - } - - fn compress_gzip(json: &Value) -> Vec { - let request_body = serde_json::to_vec(&json).unwrap(); - - let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); - encoder.write_all(&request_body).unwrap(); - encoder.finish().unwrap() - } - - fn compress_br(json: &Value) -> Vec { - let request_body = serde_json::to_vec(&json).unwrap(); - let mut result = Vec::new(); - - let params = BrotliEncoderParams::default(); - let _ = brotli::enc::BrotliCompress(&mut &request_body[..], &mut result, ¶ms).unwrap(); - - result - } - - fn compress_zstd(json: &Value) -> Vec { - let request_body = serde_json::to_vec(&json).unwrap(); - zstd::stream::encode_all(std::io::Cursor::new(request_body), 4).unwrap() - } -} diff --git a/examples/compression/src/tests.rs b/examples/compression/src/tests.rs new file mode 100644 index 0000000000..c91ccaa649 --- /dev/null +++ b/examples/compression/src/tests.rs @@ -0,0 +1,245 @@ +use assert_json_diff::assert_json_eq; +use axum::{ + body::{Body, Bytes}, + response::Response, +}; +use brotli::enc::BrotliEncoderParams; +use flate2::{read::GzDecoder, write::GzEncoder, Compression}; +use http::{header, StatusCode}; +use serde_json::{json, Value}; +use std::io::{Read, Write}; +use tower::ServiceExt; + +use super::*; + +#[tokio::test] +async fn handle_uncompressed_request_bodies() { + // Given + + let body = json(); + + let compressed_request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .body(json_body(&body)) + .unwrap(); + + // When + + let response = app().oneshot(compressed_request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + assert_json_eq!(json_from_response(response).await, json()); +} + +#[tokio::test] +async fn decompress_gzip_request_bodies() { + // Given + + let body = compress_gzip(&json()); + + let compressed_request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .header(header::CONTENT_ENCODING, "gzip") + .body(Body::from(body)) + .unwrap(); + + // When + + let response = app().oneshot(compressed_request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + assert_json_eq!(json_from_response(response).await, json()); +} + +#[tokio::test] +async fn decompress_br_request_bodies() { + // Given + + let body = compress_br(&json()); + + let compressed_request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .header(header::CONTENT_ENCODING, "br") + .body(Body::from(body)) + .unwrap(); + + // When + + let response = app().oneshot(compressed_request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + assert_json_eq!(json_from_response(response).await, json()); +} + +#[tokio::test] +async fn decompress_zstd_request_bodies() { + // Given + + let body = compress_zstd(&json()); + + let compressed_request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .header(header::CONTENT_ENCODING, "zstd") + .body(Body::from(body)) + .unwrap(); + + // When + + let response = app().oneshot(compressed_request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + assert_json_eq!(json_from_response(response).await, json()); +} + +#[tokio::test] +async fn do_not_compress_response_bodies() { + // Given + let request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .body(json_body(&json())) + .unwrap(); + + // When + + let response = app().oneshot(request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + assert_json_eq!(json_from_response(response).await, json()); +} + +#[tokio::test] +async fn compress_response_bodies_with_gzip() { + // Given + let request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .header(header::ACCEPT_ENCODING, "gzip") + .body(json_body(&json())) + .unwrap(); + + // When + + let response = app().oneshot(request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + let response_body = byte_from_response(response).await; + let mut decoder = GzDecoder::new(response_body.as_ref()); + let mut decompress_body = String::new(); + decoder.read_to_string(&mut decompress_body).unwrap(); + assert_json_eq!( + serde_json::from_str::(&decompress_body).unwrap(), + json() + ); +} + +#[tokio::test] +async fn compress_response_bodies_with_br() { + // Given + let request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .header(header::ACCEPT_ENCODING, "br") + .body(json_body(&json())) + .unwrap(); + + // When + + let response = app().oneshot(request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + let response_body = byte_from_response(response).await; + let mut decompress_body = Vec::new(); + brotli::BrotliDecompress(&mut response_body.as_ref(), &mut decompress_body).unwrap(); + assert_json_eq!( + serde_json::from_slice::(&decompress_body).unwrap(), + json() + ); +} + +#[tokio::test] +async fn compress_response_bodies_with_zstd() { + // Given + let request = http::Request::post("/") + .header(header::CONTENT_TYPE, "application/json") + .header(header::ACCEPT_ENCODING, "zstd") + .body(json_body(&json())) + .unwrap(); + + // When + + let response = app().oneshot(request).await.unwrap(); + + // Then + + assert_eq!(response.status(), StatusCode::OK); + let response_body = byte_from_response(response).await; + let decompress_body = zstd::stream::decode_all(std::io::Cursor::new(response_body)).unwrap(); + assert_json_eq!( + serde_json::from_slice::(&decompress_body).unwrap(), + json() + ); +} + +fn json() -> Value { + json!({ + "name": "foo", + "mainProduct": { + "typeId": "product", + "id": "p1" + }, + }) +} + +fn json_body(input: &Value) -> Body { + Body::from(serde_json::to_vec(&input).unwrap()) +} + +async fn json_from_response(response: Response) -> Value { + let body = byte_from_response(response).await; + body_as_json(body) +} + +async fn byte_from_response(response: Response) -> Bytes { + axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap() +} + +fn body_as_json(body: Bytes) -> Value { + serde_json::from_slice(body.as_ref()).unwrap() +} + +fn compress_gzip(json: &Value) -> Vec { + let request_body = serde_json::to_vec(&json).unwrap(); + + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&request_body).unwrap(); + encoder.finish().unwrap() +} + +fn compress_br(json: &Value) -> Vec { + let request_body = serde_json::to_vec(&json).unwrap(); + let mut result = Vec::new(); + + let params = BrotliEncoderParams::default(); + let _ = brotli::enc::BrotliCompress(&mut &request_body[..], &mut result, ¶ms).unwrap(); + + result +} + +fn compress_zstd(json: &Value) -> Vec { + let request_body = serde_json::to_vec(&json).unwrap(); + zstd::stream::encode_all(std::io::Cursor::new(request_body), 4).unwrap() +}