Repository: poem-web/poem (language: rust)

Issue:
Choose compression algorithms with priority
## Description of the feature
With middleware [Compression](https://docs.rs/poem/latest/poem/middleware/struct.Compression.html), response would be compressed if there is any algorithm specified in `Accept-Encoding` request header.

User agent may specified priority by this header like this: `Accept-Encoding: deflate, gzip;q=1.0, *;q=0.5`. But most browser would not specified priority for now.

For example, Header from Chrome would be like this: `accept-encoding: gzip, deflate, br`. And poem chose first algorithm, which means `gzip`. But usually, `Brotli` would perform better than `Gzip`, actual, all CDN that I know would chose `br` as most as they can.

Maybe we should specified default order for those algorithms, for example, `br gzip deflate`


Discussion / hints:

Fix patch:
diff --git a/poem/src/middleware/compression.rs b/poem/src/middleware/compression.rs
index 3c8f6f5438..da71080232 100644
--- a/poem/src/middleware/compression.rs
+++ b/poem/src/middleware/compression.rs
@@ -1,4 +1,4 @@
-use std::{cmp::Reverse, str::FromStr};
+use std::str::FromStr;
 
 use typed_headers::{AcceptEncoding, ContentCoding, HeaderMapExt};
 
@@ -39,6 +39,15 @@ pub struct CompressionEndpoint<E: Endpoint> {
     ep: E,
 }
 
+fn coding_priority(c: &ContentCoding) -> u8 {
+    match c {
+        &ContentCoding::DEFLATE => 1,
+        &ContentCoding::GZIP => 2,
+        &ContentCoding::BROTLI => 3,
+        _ => 0,
+    }
+}
+
 #[async_trait::async_trait]
 impl<E: Endpoint> Endpoint for CompressionEndpoint<E> {
     type Output = Response;
@@ -56,19 +65,24 @@ impl<E: Endpoint> Endpoint for CompressionEndpoint<E> {
         }
 
         // negotiate content-encoding
-        let mut compress_algo = None;
-
-        if let Ok(Some(mut encoding)) = req.headers().typed_get::<AcceptEncoding>() {
-            encoding.0.sort_by_key(|item| Reverse(item.quality));
-            if let Some(item) = encoding.0.get(0) {
-                compress_algo = match item.item {
-                    ContentCoding::BROTLI => Some(CompressionAlgo::BR),
-                    ContentCoding::DEFLATE => Some(CompressionAlgo::DEFLATE),
-                    ContentCoding::STAR | ContentCoding::GZIP => Some(CompressionAlgo::GZIP),
-                    _ => None,
-                }
-            }
-        }
+        let compress_algo = req
+            .headers()
+            .typed_get::<AcceptEncoding>()
+            .ok()
+            .flatten()
+            .and_then(|encoding| {
+                encoding
+                    .0
+                    .into_iter()
+                    .max_by_key(|item| (item.quality, coding_priority(&item.item)))
+            })
+            .map(|c| c.item)
+            .and_then(|coding| match coding {
+                ContentCoding::GZIP => Some(CompressionAlgo::GZIP),
+                ContentCoding::DEFLATE => Some(CompressionAlgo::DEFLATE),
+                ContentCoding::STAR | ContentCoding::BROTLI => Some(CompressionAlgo::BR),
+                _ => None,
+            });
 
         match compress_algo {
             Some(algo) => Ok(Compress::new(self.ep.call(req).await?, algo).into_response()),
@@ -152,10 +166,30 @@ mod tests {
             .send()
             .await;
         resp.assert_status_is_ok();
-        resp.assert_header("Content-Encoding", "gzip");
+        resp.assert_header("Content-Encoding", "br");
 
         let mut data = Vec::new();
-        let mut reader = CompressionAlgo::GZIP.decompress(resp.0.into_body().into_async_read());
+        let mut reader = CompressionAlgo::BR.decompress(resp.0.into_body().into_async_read());
+        reader.read_to_end(&mut data).await.unwrap();
+        assert_eq!(data, DATA_REV.as_bytes());
+    }
+
+    #[tokio::test]
+    async fn test_coding_priority() {
+        let ep = index.with(Compression);
+        let cli = TestClient::new(ep);
+
+        let resp = cli
+            .post("/")
+            .header("Accept-Encoding", "gzip, deflate, br")
+            .body(DATA)
+            .send()
+            .await;
+        resp.assert_status_is_ok();
+        resp.assert_header("Content-Encoding", "br");
+
+        let mut data = Vec::new();
+        let mut reader = CompressionAlgo::BR.decompress(resp.0.into_body().into_async_read());
         reader.read_to_end(&mut data).await.unwrap();
         assert_eq!(data, DATA_REV.as_bytes());
     }


Test patch: