Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions nexus/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ camino-tempfile.workspace = true
criterion.workspace = true
diesel.workspace = true
dns-server.workspace = true
flate2.workspace = true
expectorate.workspace = true
gateway-messages.workspace = true
gateway-test-utils.workspace = true
Expand Down
1 change: 1 addition & 0 deletions nexus/examples/config-second.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ external_dns_servers = ["1.1.1.1", "9.9.9.9"]
# used by `omicron-dev run-all`
bind_address = "127.0.0.1:12222"
default_request_body_max_bytes = 1048576
compression = "gzip"
# To have Nexus's external HTTP endpoint use TLS, uncomment the line below. You
# will also need to provide an initial TLS certificate during rack
# initialization. If you're using this config file, you're probably running a
Expand Down
1 change: 1 addition & 0 deletions nexus/examples/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ external_dns_servers = ["1.1.1.1", "9.9.9.9"]
# IP Address and TCP port on which to listen for the external API
bind_address = "127.0.0.1:12220"
default_request_body_max_bytes = 1048576
compression = "gzip"
# To have Nexus's external HTTP endpoint use TLS, uncomment the line below. You
# will also need to provide an initial TLS certificate during rack
# initialization. If you're using this config file, you're probably running a
Expand Down
2 changes: 2 additions & 0 deletions nexus/test-utils/src/http_testing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ impl<'a> RequestBuilder<'a> {
http::header::DATE,
http::header::LOCATION,
http::header::SET_COOKIE,
http::header::TRANSFER_ENCODING,
http::header::VARY,
http::header::HeaderName::from_static("x-request-id"),
]),
expected_response_headers: http::HeaderMap::default(),
Expand Down
1 change: 1 addition & 0 deletions nexus/tests/config.test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ external_dns_servers = ["1.1.1.1", "9.9.9.9"]
# concurrently.
bind_address = "127.0.0.1:0"
default_request_body_max_bytes = 1048576
compression = "gzip"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this turns it on for all tests. Not sure we want to do that, but for the most part it doesn't matter because I don't think requests in tests ask for gzipped responses by default.


[deployment.dropshot_internal]
bind_address = "127.0.0.1:0"
Expand Down
94 changes: 94 additions & 0 deletions nexus/tests/integration_tests/basic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -557,3 +557,97 @@ async fn test_ping(cptestctx: &ControlPlaneTestContext) {
.await;
assert_eq!(health.status, system::PingStatus::Ok);
}

/// Test that the external API returns gzip-compressed responses when the
/// client sends Accept-Encoding: gzip.
#[nexus_test]
async fn test_gzip_compression(cptestctx: &ControlPlaneTestContext) {
let client = &cptestctx.external_client;

// Create several projects so the response body exceeds the minimum
// compression threshold (512 bytes).
for i in 0..10 {
create_project(&client, &format!("project-{i}")).await;
}

// With Accept-Encoding: gzip, response should be compressed.
let response = NexusRequest::new(
RequestBuilder::new(client, Method::GET, "/v1/projects")
.header(http::header::ACCEPT_ENCODING, "gzip")
.expect_status(Some(StatusCode::OK)),
)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap();

assert_eq!(
response.headers.get(http::header::CONTENT_ENCODING).unwrap(),
"gzip",
);

// Decompress and verify the body is valid JSON.
let compressed_len = response.body.len();
let mut decoder = flate2::read::GzDecoder::new(&response.body[..]);
let mut decompressed = String::new();
std::io::Read::read_to_string(&mut decoder, &mut decompressed).unwrap();
let page: dropshot::ResultsPage<Project> =
serde_json::from_str(&decompressed).unwrap();
assert_eq!(page.items.len(), 10);

// Without Accept-Encoding: gzip, response should not be compressed.
let response = NexusRequest::object_get(client, "/v1/projects")
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap();
assert!(response.headers.get(http::header::CONTENT_ENCODING).is_none());

let uncompressed_len = response.body.len();
assert!(
compressed_len < uncompressed_len,
"compressed body ({compressed_len} bytes) should be smaller \
than uncompressed body ({uncompressed_len} bytes)"
);

// Accept-Encoding: gzip;q=0 explicitly refuses gzip, so the response
// should not be compressed even though the client mentions gzip. This
// verifies that q-value parsing is wired up correctly through the
// dropshot config.
let response = NexusRequest::new(
RequestBuilder::new(client, Method::GET, "/v1/projects")
.header(http::header::ACCEPT_ENCODING, "gzip;q=0")
.expect_status(Some(StatusCode::OK)),
)
.authn_as(AuthnMode::PrivilegedUser)
.execute()
.await
.unwrap();
assert!(response.headers.get(http::header::CONTENT_ENCODING).is_none());

// Even when a response is too small to compress, dropshot should still
// set Vary: Accept-Encoding on a compressible content type so caches
// behave correctly. /v1/ping returns a tiny JSON body well under the
// 512-byte compression threshold.
let response = NexusRequest::new(
RequestBuilder::new(client, Method::GET, "/v1/ping")
.header(http::header::ACCEPT_ENCODING, "gzip")
.expect_status(Some(StatusCode::OK)),
)
.execute()
.await
.unwrap();
assert!(response.headers.get(http::header::CONTENT_ENCODING).is_none());
let vary_values: Vec<_> = response
.headers
.get_all(http::header::VARY)
.iter()
.filter_map(|v| v.to_str().ok())
.collect();
assert!(
vary_values.iter().any(|v| v
.split(',')
.any(|entry| entry.trim().eq_ignore_ascii_case("accept-encoding"))),
"expected Vary: Accept-Encoding on small uncompressed response, got {vary_values:?}",
);
}
2 changes: 1 addition & 1 deletion sled-agent/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2409,7 +2409,7 @@ impl ServiceManager {
default_handler_task_mode:
HandlerTaskMode::Detached,
log_headers: vec![],
compression: dropshot::CompressionConfig::None,
compression: dropshot::CompressionConfig::Gzip,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this is the one that matters for production.

},
},
dropshot_internal: dropshot::ConfigDropshot {
Expand Down
Loading