Skip to content

Commit 9cbf95b

Browse files
authored
feat: compression level option support (#381)
* Make fastest compression level the default setting * Use balanced compression levels by default * Fixed formatting and addressed clippy warning * Change default zlib compression level to 3 * Updated docs to mention zlib compression * Fixed setting spelling in docs * Don't expose CompressionLevel::into_algorithm_level() * Updated documentation of the compression feature
1 parent e7bfaa2 commit 9cbf95b

File tree

12 files changed

+457
-87
lines changed

12 files changed

+457
-87
lines changed

docs/content/configuration/command-line-arguments.md

Lines changed: 216 additions & 39 deletions
Large diffs are not rendered by default.

docs/content/configuration/config-file.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ cache-control-headers = true
2828

2929
#### Auto Compression
3030
compression = true
31+
compression-level = "default"
3132

3233
#### Error pages
3334
# Note: If a relative path is used then it will be resolved under the root directory.
@@ -88,7 +89,7 @@ health = false
8889
#### Maintenance Mode
8990

9091
maintenance-mode = false
91-
# maintenance-mode-status = 503
92+
# maintenance-mode-status = 503
9293
# maintenance-mode-file = "./maintenance.html"
9394

9495
### Windows Only

docs/content/configuration/environment-variables.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,13 @@ Specify an optional CORS list of allowed HTTP headers separated by commas. It re
7878
Specify an optional CORS list of exposed HTTP headers separated by commas. It requires `SERVER_CORS_ALLOW_ORIGINS` to be used along with. Default `origin, content-type`.
7979

8080
### SERVER_COMPRESSION
81-
`Gzip`, `Deflate` or `Brotli` compression on demand determined by the `Accept-Encoding` header and applied to text-based web file types only. See [ad-hoc mime-type list](https://github.com/static-web-server/static-web-server/blob/master/src/compression.rs#L20). Default `true` (enabled).
81+
`Gzip`, `Deflate`, `Brotli` or `zlib` compression on demand determined by the `Accept-Encoding` header and applied to text-based web file types only. See [ad-hoc mime-type list](https://github.com/static-web-server/static-web-server/blob/master/src/compression.rs#L20). Default `true` (enabled).
82+
83+
### SERVER_COMPRESSION_LEVEL
84+
Supported values are `fastest` (fast compression but larger resulting files), `best` (smallest file size but potentially slow) and `default` (algorithm-specific balanced compression level). Default is `default`.
8285

8386
### SERVER_COMPRESSION_STATIC
84-
Look up the pre-compressed file variant (`.gz` or `.br`) on disk of a requested file and serves it directly if available. Default `false` (disabled). The compression type is determined by the `Accept-Encoding` header.
87+
Look up the pre-compressed file variant (`.gz`, `.br` or `.zst`) on disk of a requested file and serves it directly if available. Default `false` (disabled). The compression type is determined by the `Accept-Encoding` header.
8588

8689
### SERVER_DIRECTORY_LISTING
8790
Enable directory listing for all requests ending with the slash character (‘/’). Default `false` (disabled).

docs/content/features/compression.md

Lines changed: 20 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,44 +2,37 @@
22

33
**`SWS`** provides [`Gzip`](https://datatracker.ietf.org/doc/html/rfc1952), [`Deflate`](https://datatracker.ietf.org/doc/html/rfc1951#section-Abstract), [`Brotli`](https://www.ietf.org/rfc/rfc7932.txt) and [`Zstandard` (zstd)](https://datatracker.ietf.org/doc/html/rfc8878) compression of HTTP responses.
44

5-
The compression functionality is determined by the [`Accept-Encoding`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header and is only applied to text-based web file types.
5+
This feature is enabled by default and can be controlled by the boolean `-x, --compression` option or the equivalent [SERVER_COMPRESSION](../configuration/environment-variables.md#server_compression) env.
6+
7+
```sh
8+
static-web-server \
9+
--port 8787 \
10+
--root ./my-public-dir \
11+
--compression true
12+
```
13+
14+
## Choice of compression algorithm
15+
16+
The compression algorithm is determined by the [`Accept-Encoding`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding) header and the compression support built into SWS. By default SWS builds with support for `Gzip`, `Deflate`, `Brotli` and `Zstandard` algorithms.
617

718
## MIME types compressed
819

9-
Only this list of common text-based MIME-type files will be compressed either with `Gzip`, `Deflate` or `Brotli` via the `Accept-Encoding` header value.
20+
Compression is only applied to files with the MIME types listed below, indicating text and similarly well compressing formats. The asterisk `*` is a placeholder indicating an arbitrary MIME type part.
1021

1122
```txt
12-
text/html
13-
text/css
14-
text/javascript
15-
text/xml
16-
text/plain
17-
text/csv
18-
text/calendar
19-
text/markdown
20-
text/x-yaml
21-
text/x-toml
22-
text/x-component
23+
text/*
24+
*+xml
25+
*+json
2326
application/rtf
24-
application/xhtml+xml
2527
application/javascript
26-
application/x-javascript
2728
application/json
2829
application/xml
29-
application/rss+xml
30-
application/atom+xml
30+
font/ttf
31+
application/font-sfnt
3132
application/vnd.ms-fontobject
32-
font/truetype
33-
font/opentype
34-
image/svg+xml
3533
application/wasm
3634
```
3735

38-
This feature is enabled by default and can be controlled by the boolean `-x, --compression` option or the equivalent [SERVER_COMPRESSION](./../configuration/environment-variables.md#server_compression) env.
36+
## Compression level
3937

40-
```sh
41-
static-web-server \
42-
--port 8787 \
43-
--root ./my-public-dir \
44-
--compression true
45-
```
38+
SWS allows selecting the compression level via `--compression-level` command line option or the equivalent [SERVER_COMPRESSION_LEVEL](../configuration/environment-variables.md#server_compression_level) env. The available values are `fastest`, `best` and `default`. `fastest` will result in the lowest CPU load but also the worst compression factor. `best` will attempt to compress the data as much as possible (not recommended with `Brotli` or `Zstandard` compression, will be very slow). `default` tries to strike a balance, choosing a compression level where compression factor is already fairly good but the CPU load is still low.

src/compression.rs

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ use crate::{
3737
handler::RequestHandlerOpts,
3838
headers_ext::{AcceptEncoding, ContentCoding},
3939
http_ext::MethodExt,
40+
settings::CompressionLevel,
4041
Error, Result,
4142
};
4243

@@ -67,8 +68,9 @@ const AVAILABLE_ENCODINGS: &[ContentCoding] = &[
6768
];
6869

6970
/// Initializes dynamic compression.
70-
pub fn init(enabled: bool, handler_opts: &mut RequestHandlerOpts) {
71+
pub fn init(enabled: bool, level: CompressionLevel, handler_opts: &mut RequestHandlerOpts) {
7172
handler_opts.compression = enabled;
73+
handler_opts.compression_level = level;
7274

7375
const FORMATS: &[&str] = &[
7476
#[cfg(any(feature = "compression", feature = "compression-deflate"))]
@@ -81,7 +83,7 @@ pub fn init(enabled: bool, handler_opts: &mut RequestHandlerOpts) {
8183
"zstd",
8284
];
8385
server_info!(
84-
"auto compression: enabled={enabled}, formats={}",
86+
"auto compression: enabled={enabled}, formats={}, compression level={level:?}",
8587
FORMATS.join(",")
8688
);
8789
}
@@ -108,7 +110,7 @@ pub(crate) fn post_process(
108110
);
109111

110112
// Auto compression based on the `Accept-Encoding` header
111-
match auto(req.method(), req.headers(), resp) {
113+
match auto(req.method(), req.headers(), opts.compression_level, resp) {
112114
Ok(resp) => Ok(resp),
113115
Err(err) => {
114116
tracing::error!("error during body compression: {:?}", err);
@@ -130,6 +132,7 @@ pub(crate) fn post_process(
130132
pub fn auto(
131133
method: &Method,
132134
headers: &HeaderMap<HeaderValue>,
135+
level: CompressionLevel,
133136
resp: Response<Body>,
134137
) -> Result<Response<Body>> {
135138
// Skip compression for HEAD and OPTIONS request methods
@@ -154,25 +157,25 @@ pub fn auto(
154157
#[cfg(any(feature = "compression", feature = "compression-gzip"))]
155158
if encoding == ContentCoding::GZIP {
156159
let (head, body) = resp.into_parts();
157-
return Ok(gzip(head, body.into()));
160+
return Ok(gzip(head, body.into(), level));
158161
}
159162

160163
#[cfg(any(feature = "compression", feature = "compression-deflate"))]
161164
if encoding == ContentCoding::DEFLATE {
162165
let (head, body) = resp.into_parts();
163-
return Ok(deflate(head, body.into()));
166+
return Ok(deflate(head, body.into(), level));
164167
}
165168

166169
#[cfg(any(feature = "compression", feature = "compression-brotli"))]
167170
if encoding == ContentCoding::BROTLI {
168171
let (head, body) = resp.into_parts();
169-
return Ok(brotli(head, body.into()));
172+
return Ok(brotli(head, body.into(), level));
170173
}
171174

172175
#[cfg(any(feature = "compression", feature = "compression-zstd"))]
173176
if encoding == ContentCoding::ZSTD {
174177
let (head, body) = resp.into_parts();
175-
return Ok(zstd(head, body.into()));
178+
return Ok(zstd(head, body.into(), level));
176179
}
177180

178181
tracing::trace!("no compression feature matched the preferred encoding, probably not enabled or unsupported");
@@ -200,10 +203,17 @@ fn is_text(mime: Mime) -> bool {
200203
pub fn gzip(
201204
mut head: http::response::Parts,
202205
body: CompressableBody<Body, hyper::Error>,
206+
level: CompressionLevel,
203207
) -> Response<Body> {
208+
const DEFAULT_COMPRESSION_LEVEL: i32 = 4;
209+
204210
tracing::trace!("compressing response body on the fly using GZIP");
205211

206-
let body = Body::wrap_stream(ReaderStream::new(GzipEncoder::new(StreamReader::new(body))));
212+
let level = level.into_algorithm_level(DEFAULT_COMPRESSION_LEVEL);
213+
let body = Body::wrap_stream(ReaderStream::new(GzipEncoder::with_quality(
214+
StreamReader::new(body),
215+
level,
216+
)));
207217
let header = create_encoding_header(head.headers.remove(CONTENT_ENCODING), ContentCoding::GZIP);
208218
head.headers.remove(CONTENT_LENGTH);
209219
head.headers.append(CONTENT_ENCODING, header);
@@ -220,12 +230,17 @@ pub fn gzip(
220230
pub fn deflate(
221231
mut head: http::response::Parts,
222232
body: CompressableBody<Body, hyper::Error>,
233+
level: CompressionLevel,
223234
) -> Response<Body> {
235+
const DEFAULT_COMPRESSION_LEVEL: i32 = 4;
236+
224237
tracing::trace!("compressing response body on the fly using DEFLATE");
225238

226-
let body = Body::wrap_stream(ReaderStream::new(DeflateEncoder::new(StreamReader::new(
227-
body,
228-
))));
239+
let level = level.into_algorithm_level(DEFAULT_COMPRESSION_LEVEL);
240+
let body = Body::wrap_stream(ReaderStream::new(DeflateEncoder::with_quality(
241+
StreamReader::new(body),
242+
level,
243+
)));
229244
let header = create_encoding_header(
230245
head.headers.remove(CONTENT_ENCODING),
231246
ContentCoding::DEFLATE,
@@ -245,12 +260,17 @@ pub fn deflate(
245260
pub fn brotli(
246261
mut head: http::response::Parts,
247262
body: CompressableBody<Body, hyper::Error>,
263+
level: CompressionLevel,
248264
) -> Response<Body> {
265+
const DEFAULT_COMPRESSION_LEVEL: i32 = 4;
266+
249267
tracing::trace!("compressing response body on the fly using BROTLI");
250268

251-
let body = Body::wrap_stream(ReaderStream::new(BrotliEncoder::new(StreamReader::new(
252-
body,
253-
))));
269+
let level = level.into_algorithm_level(DEFAULT_COMPRESSION_LEVEL);
270+
let body = Body::wrap_stream(ReaderStream::new(BrotliEncoder::with_quality(
271+
StreamReader::new(body),
272+
level,
273+
)));
254274
let header =
255275
create_encoding_header(head.headers.remove(CONTENT_ENCODING), ContentCoding::BROTLI);
256276
head.headers.remove(CONTENT_LENGTH);
@@ -268,10 +288,17 @@ pub fn brotli(
268288
pub fn zstd(
269289
mut head: http::response::Parts,
270290
body: CompressableBody<Body, hyper::Error>,
291+
level: CompressionLevel,
271292
) -> Response<Body> {
293+
const DEFAULT_COMPRESSION_LEVEL: i32 = 3;
294+
272295
tracing::trace!("compressing response body on the fly using ZSTD");
273296

274-
let body = Body::wrap_stream(ReaderStream::new(ZstdEncoder::new(StreamReader::new(body))));
297+
let level = level.into_algorithm_level(DEFAULT_COMPRESSION_LEVEL);
298+
let body = Body::wrap_stream(ReaderStream::new(ZstdEncoder::with_quality(
299+
StreamReader::new(body),
300+
level,
301+
)));
275302
let header = create_encoding_header(head.headers.remove(CONTENT_ENCODING), ContentCoding::ZSTD);
276303
head.headers.remove(CONTENT_LENGTH);
277304
head.headers.append(CONTENT_ENCODING, header);

src/handler.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,15 @@ pub struct RequestHandlerOpts {
4646
pub root_dir: PathBuf,
4747
/// Compression feature.
4848
pub compression: bool,
49+
#[cfg(any(
50+
feature = "compression",
51+
feature = "compression-gzip",
52+
feature = "compression-brotli",
53+
feature = "compression-zstd",
54+
feature = "compression-deflate"
55+
))]
56+
/// Compression level.
57+
pub compression_level: crate::settings::CompressionLevel,
4958
/// Compression static feature.
5059
pub compression_static: bool,
5160
/// Directory listing feature.
@@ -108,6 +117,14 @@ impl Default for RequestHandlerOpts {
108117
root_dir: PathBuf::from("./public"),
109118
compression: true,
110119
compression_static: false,
120+
#[cfg(any(
121+
feature = "compression",
122+
feature = "compression-gzip",
123+
feature = "compression-brotli",
124+
feature = "compression-zstd",
125+
feature = "compression-deflate"
126+
))]
127+
compression_level: crate::settings::CompressionLevel::Default,
111128
#[cfg(feature = "directory-listing")]
112129
dir_listing: false,
113130
#[cfg(feature = "directory-listing")]

src/server.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,11 @@ impl Server {
317317
feature = "compression-brotli",
318318
feature = "compression-zstd",
319319
))]
320-
compression::init(general.compression, &mut handler_opts);
320+
compression::init(
321+
general.compression,
322+
general.compression_level,
323+
&mut handler_opts,
324+
);
321325

322326
// Cache control headers option
323327
control_headers::init(general.cache_control_headers, &mut handler_opts);

src/settings/cli.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,28 @@ pub struct General {
258258
)]
259259
/// Gzip, Deflate, Brotli or Zstd compression on demand determined by the Accept-Encoding header and applied to text-based web file types only.
260260
pub compression: bool,
261+
262+
#[cfg(any(
263+
feature = "compression",
264+
feature = "compression-gzip",
265+
feature = "compression-brotli",
266+
feature = "compression-zstd",
267+
feature = "compression-deflate"
268+
))]
269+
#[cfg_attr(
270+
docsrs,
271+
doc(cfg(any(
272+
feature = "compression",
273+
feature = "compression-gzip",
274+
feature = "compression-brotli",
275+
feature = "compression-zstd",
276+
feature = "compression-deflate"
277+
)))
278+
)]
279+
#[arg(long, default_value = "default", env = "SERVER_COMPRESSION_LEVEL")]
280+
/// Compression level to apply for Gzip, Deflate, Brotli or Zstd compression.
281+
pub compression_level: super::CompressionLevel,
282+
261283
#[cfg(any(
262284
feature = "compression",
263285
feature = "compression-gzip",

0 commit comments

Comments
 (0)