Skip to content

Feature/144 gzip compression filter#149

Open
DennSel wants to merge 26 commits intomainfrom
feature/144-gzip-compression-filter
Open

Feature/144 gzip compression filter#149
DennSel wants to merge 26 commits intomainfrom
feature/144-gzip-compression-filter

Conversation

@DennSel
Copy link

@DennSel DennSel commented Feb 27, 2026

Closes #144

Implemented a CompressionFilter that compresses HTTP response bodies using GZIP encoding.

Changes:

  • CompressionFilter – compresses responses when client sends Accept-Encoding: gzip
  • CompressionConfig – reads compression settings from ConfigLoader
  • ConfigLoader – extended with compression fields, defaults, and YAML parsing
  • application-properties.yml – added compression block

Results:

  • 2196 bytes → 826 bytes (62% reduction) on index page

Summary by CodeRabbit

  • New Features

    • Configurable HTTP response compression (gzip) with enable/disable toggle and adjustable minimum size; responses that meet criteria are compressed and Content-Encoding and Vary headers updated.
    • New application configuration defaults and properties to control compression.
  • Tests

    • Added comprehensive tests covering enabled/disabled states, Accept-Encoding variants (case and q-values), size-threshold edge cases, and header/body outcomes.

@DennSel DennSel linked an issue Feb 27, 2026 that may be closed by this pull request
@DennSel DennSel self-assigned this Feb 27, 2026
@coderabbitai
Copy link

coderabbitai bot commented Feb 27, 2026

📝 Walkthrough

Walkthrough

Adds GZIP response compression: new config fields and YAML defaults, a CompressionConfig wrapper, a Global CompressionFilter that gzips responses when client accepts gzip and body ≥ threshold, and unit tests covering parsing, thresholds, and header behavior.

Changes

Cohort / File(s) Summary
Configuration
src/main/java/org/juv25d/config/CompressionConfig.java, src/main/java/org/juv25d/util/ConfigLoader.java, src/main/resources/application-properties.yml
New CompressionConfig wrapper; ConfigLoader gains compressionEnabled and minCompressSize fields with accessors (isCompressionEnabled(), getMinCompressSize()); YAML adds compression.enabled and compression.min-compress-size defaults.
Compression Filter
src/main/java/org/juv25d/filter/CompressionFilter.java
New @Global(order = 10) CompressionFilter with config/default constructors; checks Accept-Encoding (including q-values), consults config and size threshold, gzips response bodies ≥ threshold, sets Content-Encoding: gzip, and updates Vary: Accept-Encoding.
Tests
src/test/java/org/juv25d/filter/CompressionFilterTest.java
New unit tests covering enabled/disabled behavior, Accept-Encoding parsing (case/quality), threshold boundaries (below, equal, above), compression outcome, header updates, and gzip integrity verification.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Filter as CompressionFilter
    participant Chain as FilterChain
    participant Config as ConfigLoader
    participant Gzip as GzipProcessor

    Client->>Filter: HTTP Request (may include Accept-Encoding: gzip)
    Filter->>Config: isCompressionEnabled(), getMinCompressSize()
    alt disabled or client doesn't accept gzip
        Filter->>Chain: doFilter(req,res)
        Chain-->>Filter: Response (uncompressed)
        Filter->>Client: Response (uncompressed)
    else enabled and client accepts gzip
        Filter->>Chain: doFilter(req,res)
        Chain-->>Filter: Response Body (bytes)
        alt Body size >= minCompressSize
            Filter->>Gzip: compress(body)
            Gzip-->>Filter: compressed bytes
            Filter->>Filter: set Content-Encoding: gzip
            Filter->>Filter: set/append Vary: Accept-Encoding
            Filter->>Client: Compressed Response
        else Body size < minCompressSize
            Filter->>Client: Uncompressed Response
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • jesperlarsson1910
  • EmmaTravljanin

Poem

🐰 I nibble bytes beneath a moonlit log,

I tuck them tight within a gzip cog,
Headers hop on, parcels trimmed and light,
I send them off with a joyful flight,
Hooray — small packets through the night!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/144 gzip compression filter' clearly describes the main change: adding a GZIP compression filter feature, with issue reference for tracking.
Linked Issues check ✅ Passed The pull request fully implements all coding requirements from issue #144: GZIP compression using GZIPOutputStream [#144], Accept-Encoding header inspection with quality values [#144], Content-Encoding header management to prevent double compression [#144], and configuration option support [#144].
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing the GZIP compression feature: configuration support classes, filter implementation, configuration file updates, and comprehensive test coverage.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/144-gzip-compression-filter

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/test/java/org/juv25d/filter/CompressionFilterTest.java (2)

60-75: Assert compressed payload integrity, not only method calls

Current tests can pass even if setBody receives invalid bytes. Capture the body passed to setBody, decompress it, and assert round-trip equality.

✅ Suggested test pattern
+import org.mockito.ArgumentCaptor;
+import java.util.zip.GZIPInputStream;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
@@
         filter.doFilter(req, res, chain);

-        verify(res).setBody(any(byte[].class));
+        ArgumentCaptor<byte[]> captor = ArgumentCaptor.forClass(byte[].class);
+        verify(res).setBody(captor.capture());
+        byte[] inflated = gunzip(captor.getValue());
+        assertArrayEquals(body, inflated);
         verify(res).setHeader("Content-Encoding", "gzip");
@@
+    private byte[] gunzip(byte[] gz) throws IOException {
+        try (GZIPInputStream in = new GZIPInputStream(new ByteArrayInputStream(gz));
+             ByteArrayOutputStream out = new ByteArrayOutputStream()) {
+            in.transferTo(out);
+            return out.toByteArray();
+        }
+    }

Also applies to: 93-121

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/filter/CompressionFilterTest.java` around lines 60 -
75, Test currently only verifies that res.setBody was called without checking
payload correctness; update shouldCompress_whenAcceptEncodingIsGzip in
CompressionFilterTest to capture the byte[] argument passed to res.setBody
(e.g., with an ArgumentCaptor<byte[]> or Mockito.argThat) after calling
CompressionFilter.doFilter, then decompress that captured byte[] using
GZIPInputStream and assert the decompressed bytes equal the original
uncompressed body; do the same for the other tests mentioned (lines 93-121) that
verify setBody to ensure round-trip integrity rather than only method calls,
referencing CompressionFilter.doFilter and res.setBody in each updated test.

25-122: Add regression tests for gzip;q= and pre-encoded responses

Please add cases for Accept-Encoding: gzip;q=1.0 and when response already has Content-Encoding set, to lock in correct behavior and prevent double compression regressions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/filter/CompressionFilterTest.java` around lines 25 -
122, Add two tests inside CompressionFilterTest: one that constructs
CompressionFilter (e.g., new CompressionFilter(true, 100)), stubs req.headers()
to include "Accept-Encoding" -> "gzip;q=1.0", stubs res.body() to return a
compressible byte[] and asserts doFilter calls res.setBody(...) and sets
"Content-Encoding" to "gzip"; and another that stubs req.headers() to accept
gzip but stubs res.header("Content-Encoding") (or the response's existing
headers) to a non-null value before calling doFilter and asserts that
res.setBody(...) is NOT called (no additional compression) and existing
Content-Encoding is preserved. Use the existing symbols CompressionFilter,
doFilter(req, res, chain), req.headers(), res.body(), res.setBody(...), and
res.setHeader(...) to locate and implement these tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/juv25d/filter/CompressionFilter.java`:
- Around line 61-69: The acceptsGzip method is too strict: it uses a
case-sensitive header lookup and only matches exact "gzip", so values like
"gzip;q=1.0" or different header-casing are missed. Update acceptsGzip to
retrieve the Accept-Encoding header in a case-insensitive way (or normalize
keys) and, when scanning tokens from acceptEncoding.split(","), trim and compare
the token's media-type portion case-insensitively (e.g. split each token on ';'
and check tokenPart.equalsIgnoreCase("gzip") or tokenPart.startsWith("gzip") for
robustness) instead of requiring an exact match; keep the null/empty guard and
anyStream logic around the tokens in the acceptsGzip method.
- Around line 42-45: In CompressionFilter, avoid double-compressing by checking
the response's existing "Content-Encoding" header before calling
compress(body)/res.setBody; if a non-empty Content-Encoding exists, skip
compression and do not set that header. When setting the "Vary" header, merge
with any existing Vary values instead of overwriting: read
res.getHeader("Vary"), append "Accept-Encoding" only if not already present, and
then update the header (use res.setHeader or equivalent). Ensure
res.setHeader("Content-Encoding","gzip") is only called when compression
actually occurs.

In `@src/main/java/org/juv25d/util/ConfigLoader.java`:
- Around line 126-127: The assignment to this.minCompressSize in ConfigLoader
currently parses compressionConfig.getOrDefault("min-compress-size", 1024)
directly and can accept negative values; change this to parse the value safely
(catch NumberFormatException), then enforce a lower bound (e.g. clamp to 0 or a
sensible default like 1024 if the parsed value is negative or invalid) before
assigning to the minCompressSize field so negative config cannot force
unintended compression.

---

Nitpick comments:
In `@src/test/java/org/juv25d/filter/CompressionFilterTest.java`:
- Around line 60-75: Test currently only verifies that res.setBody was called
without checking payload correctness; update
shouldCompress_whenAcceptEncodingIsGzip in CompressionFilterTest to capture the
byte[] argument passed to res.setBody (e.g., with an ArgumentCaptor<byte[]> or
Mockito.argThat) after calling CompressionFilter.doFilter, then decompress that
captured byte[] using GZIPInputStream and assert the decompressed bytes equal
the original uncompressed body; do the same for the other tests mentioned (lines
93-121) that verify setBody to ensure round-trip integrity rather than only
method calls, referencing CompressionFilter.doFilter and res.setBody in each
updated test.
- Around line 25-122: Add two tests inside CompressionFilterTest: one that
constructs CompressionFilter (e.g., new CompressionFilter(true, 100)), stubs
req.headers() to include "Accept-Encoding" -> "gzip;q=1.0", stubs res.body() to
return a compressible byte[] and asserts doFilter calls res.setBody(...) and
sets "Content-Encoding" to "gzip"; and another that stubs req.headers() to
accept gzip but stubs res.header("Content-Encoding") (or the response's existing
headers) to a non-null value before calling doFilter and asserts that
res.setBody(...) is NOT called (no additional compression) and existing
Content-Encoding is preserved. Use the existing symbols CompressionFilter,
doFilter(req, res, chain), req.headers(), res.body(), res.setBody(...), and
res.setHeader(...) to locate and implement these tests.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e46bb6b and ba0eff8.

📒 Files selected for processing (5)
  • src/main/java/org/juv25d/config/CompressionConfig.java
  • src/main/java/org/juv25d/filter/CompressionFilter.java
  • src/main/java/org/juv25d/util/ConfigLoader.java
  • src/main/resources/application-properties.yml
  • src/test/java/org/juv25d/filter/CompressionFilterTest.java

Johan-jans
Johan-jans previously approved these changes Feb 27, 2026
Copy link

@Johan-jans Johan-jans left a comment

Choose a reason for hiding this comment

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

Nice implementation, clean structure and good test coverage

Copy link

@OskarLundqvist33 OskarLundqvist33 left a comment

Choose a reason for hiding this comment

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

Great work, looks really clean and a great implementation!

One small nitpick, in src/main/java/org/juv25d/filter/CompressionFilter.java on row 44-45 there could be a small potential issue. This doesn't check weather Content-Encoding was already present before the compression, and if some other filter or handler already compressed (or otherwise encoded) the response, this filter would double compress it, which could be bad. Could probably just add a check to see if Content-Encoding header already exists to avoid double compression risk. Small risk but could be a nice safety. Otherwise awesome work!

@DennSel DennSel dismissed stale reviews from OskarLundqvist33 and Johan-jans via 749a170 March 1, 2026 17:47
@DennSel
Copy link
Author

DennSel commented Mar 1, 2026

Great work, looks really clean and a great implementation!

One small nitpick, in src/main/java/org/juv25d/filter/CompressionFilter.java on row 44-45 there could be a small potential issue. This doesn't check weather Content-Encoding was already present before the compression, and if some other filter or handler already compressed (or otherwise encoded) the response, this filter would double compress it, which could be bad. Could probably just add a check to see if Content-Encoding header already exists to avoid double compression risk. Small risk but could be a nice safety. Otherwise awesome work!

Good catch, added a skip before compression:
if (res.getHeader("Content-Encoding") != null) {
return;
}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/main/java/org/juv25d/filter/CompressionFilter.java (1)

43-56: ⚠️ Potential issue | 🟡 Minor

Harden Content-Encoding/Vary handling to avoid false skips and duplicate Vary values.

At Line 43, a blank Content-Encoding still blocks compression. At Line 53, Accept-Encoding is appended even if already present, producing duplicate Vary tokens.

🔧 Suggested fix
-        if (res.getHeader("Content-Encoding") != null) {
+        String existingEncoding = res.getHeader("Content-Encoding");
+        if (existingEncoding != null && !existingEncoding.isBlank()) {
             return;
         }
@@
-        String existingVary = res.getHeader("Vary");
-        if (existingVary != null && !existingVary.isEmpty()) {
-            res.setHeader("Vary", existingVary + ", Accept-Encoding");
-        } else {
-            res.setHeader("Vary", "Accept-Encoding");
-        }
+        String existingVary = res.getHeader("Vary");
+        if (existingVary == null || existingVary.isBlank()) {
+            res.setHeader("Vary", "Accept-Encoding");
+        } else if (Arrays.stream(existingVary.split(","))
+            .map(String::trim)
+            .noneMatch(v -> v.equalsIgnoreCase("Accept-Encoding"))) {
+            res.setHeader("Vary", existingVary + ", Accept-Encoding");
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/CompressionFilter.java` around lines 43 - 56,
The current logic in CompressionFilter incorrectly skips compression when
Content-Encoding is present but blank and blindly appends "Accept-Encoding" to
Vary causing duplicates; change the Content-Encoding check to read the header
into a variable and only return if the header is non-null and contains
non-whitespace characters (e.g., header != null && !header.trim().isEmpty());
for the Vary handling, parse the existing Vary value from res.getHeader("Vary")
into comma-separated tokens, trim and compare case-insensitively for
"Accept-Encoding", and only append ", Accept-Encoding" (using res.setHeader)
when that token is not already present, otherwise leave the header unchanged
(use the existing compress(body), res.setBody and res.setHeader calls in
CompressionFilter).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/juv25d/filter/CompressionFilter.java`:
- Around line 82-85: The Accept-Encoding parsing in CompressionFilter currently
treats entries like "gzip;q=0" as acceptable; update the logic that processes
acceptEncoding (the stream mapping that splits on ";" and trims) to parse any
"q=" parameter for each encoding and treat q=0 as unacceptable—i.e., for each
entry split into token and parameters, parse the q value (default 1.0 if
missing) and only consider the token equalIgnoreCase("gzip") when its q value is
> 0; adjust the stream to filter out q==0 entries before anyMatch.

---

Duplicate comments:
In `@src/main/java/org/juv25d/filter/CompressionFilter.java`:
- Around line 43-56: The current logic in CompressionFilter incorrectly skips
compression when Content-Encoding is present but blank and blindly appends
"Accept-Encoding" to Vary causing duplicates; change the Content-Encoding check
to read the header into a variable and only return if the header is non-null and
contains non-whitespace characters (e.g., header != null &&
!header.trim().isEmpty()); for the Vary handling, parse the existing Vary value
from res.getHeader("Vary") into comma-separated tokens, trim and compare
case-insensitively for "Accept-Encoding", and only append ", Accept-Encoding"
(using res.setHeader) when that token is not already present, otherwise leave
the header unchanged (use the existing compress(body), res.setBody and
res.setHeader calls in CompressionFilter).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 79463fb and 749a170.

📒 Files selected for processing (1)
  • src/main/java/org/juv25d/filter/CompressionFilter.java

Parse quality values in Accept-Encoding header to respect client preferences.
Skip compression when gzip;q=0 is specified by client.
Add payload integrity verification using round-trip decompression in tests.

Fixes:
- Respect Accept-Encoding quality values (gzip;q=0 should not compress)
- Verify compressed data can be decompressed correctly
- Prevent double compression when Content-Encoding already exists
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/main/java/org/juv25d/filter/CompressionFilter.java (2)

106-113: Consider using try-with-resources for consistency.

While ByteArrayOutputStream doesn't hold external resources, using try-with-resources ensures the GZIPOutputStream is closed even if an exception occurs during write().

♻️ Suggested refactor
-    private byte [] compress(byte [] data) throws IOException {
-        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
-        GZIPOutputStream gzipstream = new GZIPOutputStream(byteStream);
-
-        gzipstream.write(data);
-        gzipstream.close();
-        return byteStream.toByteArray();
+    private byte[] compress(byte[] data) throws IOException {
+        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
+        try (GZIPOutputStream gzipStream = new GZIPOutputStream(byteStream)) {
+            gzipStream.write(data);
+        }
+        return byteStream.toByteArray();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/CompressionFilter.java` around lines 106 -
113, The compress(byte [] data) method currently constructs a
ByteArrayOutputStream and GZIPOutputStream but closes the GZIPOutputStream only
on the happy path; change it to use try-with-resources to ensure
GZIPOutputStream (and optionally ByteArrayOutputStream) are closed on
exceptions. Locate the compress method and wrap creation of GZIPOutputStream in
a try-with-resources block (using GZIPOutputStream gzip = new
GZIPOutputStream(byteStream)) so gzip.write(data) is executed inside the try and
the method returns byteStream.toByteArray() after the try completes.

51-56: Vary header may contain duplicate Accept-Encoding values.

The current logic appends , Accept-Encoding when a Vary header exists, but doesn't check if it already contains Accept-Encoding. This could result in Vary: Accept-Encoding, Accept-Encoding if another filter set the Vary header with that value.

♻️ Suggested fix to prevent duplicates
         String existingVary = res.getHeader("Vary");
         if (existingVary != null && !existingVary.isEmpty()) {
-            res.setHeader("Vary", existingVary + ", Accept-Encoding");
+            boolean alreadyHasAcceptEncoding = Arrays.stream(existingVary.split(","))
+                .map(String::trim)
+                .anyMatch(v -> v.equalsIgnoreCase("Accept-Encoding"));
+            if (!alreadyHasAcceptEncoding) {
+                res.setHeader("Vary", existingVary + ", Accept-Encoding");
+            }
         } else {
             res.setHeader("Vary", "Accept-Encoding");
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/CompressionFilter.java` around lines 51 - 56,
The Vary header handling in CompressionFilter currently appends ",
Accept-Encoding" without checking for existing tokens; update the logic in the
method where res.getHeader("Vary") is read (in CompressionFilter) to first parse
existingVary into comma-separated tokens, trim and compare tokens
case-insensitively to see if "Accept-Encoding" is already present, and only
append it when absent; then call res.setHeader("Vary", ... ) with either the
original value or the original value plus ", Accept-Encoding" (ensuring proper
trimming and a single separator) to avoid duplicate Accept-Encoding entries.
src/test/java/org/juv25d/filter/CompressionFilterTest.java (1)

167-173: LGTM!

The gunzip helper is correctly implemented for test verification.

Consider adding tests for:

  • Double-compression prevention (when Content-Encoding already exists)
  • Vary header merging (when Vary header already exists)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/juv25d/filter/CompressionFilterTest.java` around lines 167
- 173, Add two unit tests in CompressionFilterTest to cover the scenarios
mentioned: 1) testDoubleCompressionPrevention should exercise the filter with a
response that already has a Content-Encoding header set and assert that the
filter does not apply gzip compression (use the existing gunzip helper to
validate no double-compression and verify Content-Encoding remains unchanged).
2) testVaryHeaderMerging should exercise the filter when the response already
contains a Vary header and assert that the filter merges its own Vary value with
the existing one rather than overwriting it (verify final Vary contains both
values). Use existing test helpers and the gunzip method to inspect response
payload where applicable and reuse request/response setup patterns from other
tests in this class.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/juv25d/filter/CompressionFilter.java`:
- Around line 79-87: Run the code formatter to fix Spotless violations in
CompressionFilter.java: run `mvn spotless:apply` and commit the resulting
changes; specifically reformat the method that checks Accept-Encoding (the logic
using acceptEncoding,
Arrays.stream(...).map(String::trim).filter(this::isGzipWithQualityAboveZero).anyMatch(...))
so it conforms to project style (spacing, line breaks, imports) and re-run
Spotless to ensure no formatting errors remain before pushing.
- Around line 72-87: In acceptsGzip, guard against a null header name by
changing the header key check to ensure e.getKey() != null before calling
equalsIgnoreCase, and simplify the encoding stream by removing the redundant
map/filter/anyMatch sequence: after splitting and trimming the encodings,
directly use anyMatch(this::isGzipWithQualityAboveZero) instead of filtering
then checking equalsIgnoreCase("gzip") because isGzipWithQualityAboveZero
already enforces gzip and quality>0; refer to the acceptsGzip method and
isGzipWithQualityAboveZero when making these changes.

---

Nitpick comments:
In `@src/main/java/org/juv25d/filter/CompressionFilter.java`:
- Around line 106-113: The compress(byte [] data) method currently constructs a
ByteArrayOutputStream and GZIPOutputStream but closes the GZIPOutputStream only
on the happy path; change it to use try-with-resources to ensure
GZIPOutputStream (and optionally ByteArrayOutputStream) are closed on
exceptions. Locate the compress method and wrap creation of GZIPOutputStream in
a try-with-resources block (using GZIPOutputStream gzip = new
GZIPOutputStream(byteStream)) so gzip.write(data) is executed inside the try and
the method returns byteStream.toByteArray() after the try completes.
- Around line 51-56: The Vary header handling in CompressionFilter currently
appends ", Accept-Encoding" without checking for existing tokens; update the
logic in the method where res.getHeader("Vary") is read (in CompressionFilter)
to first parse existingVary into comma-separated tokens, trim and compare tokens
case-insensitively to see if "Accept-Encoding" is already present, and only
append it when absent; then call res.setHeader("Vary", ... ) with either the
original value or the original value plus ", Accept-Encoding" (ensuring proper
trimming and a single separator) to avoid duplicate Accept-Encoding entries.

In `@src/test/java/org/juv25d/filter/CompressionFilterTest.java`:
- Around line 167-173: Add two unit tests in CompressionFilterTest to cover the
scenarios mentioned: 1) testDoubleCompressionPrevention should exercise the
filter with a response that already has a Content-Encoding header set and assert
that the filter does not apply gzip compression (use the existing gunzip helper
to validate no double-compression and verify Content-Encoding remains
unchanged). 2) testVaryHeaderMerging should exercise the filter when the
response already contains a Vary header and assert that the filter merges its
own Vary value with the existing one rather than overwriting it (verify final
Vary contains both values). Use existing test helpers and the gunzip method to
inspect response payload where applicable and reuse request/response setup
patterns from other tests in this class.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 749a170 and 5d6df15.

📒 Files selected for processing (2)
  • src/main/java/org/juv25d/filter/CompressionFilter.java
  • src/test/java/org/juv25d/filter/CompressionFilterTest.java

Comment on lines +72 to +87
private boolean acceptsGzip(HttpRequest req) {
String acceptEncoding = req.headers().entrySet().stream()
.filter(e -> e.getKey().equalsIgnoreCase("Accept-Encoding"))
.map(Map.Entry::getValue)
.findFirst()
.orElse(null);

if (acceptEncoding == null || acceptEncoding.isEmpty()) {
return false;
}

return Arrays.stream(acceptEncoding.split(","))
.map(String::trim)
.filter(this::isGzipWithQualityAboveZero)
.anyMatch(e -> e.split(";")[0].trim().equalsIgnoreCase("gzip"));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential NPE and redundant filtering logic.

Two issues in acceptsGzip:

  1. Line 74: e.getKey().equalsIgnoreCase(...) may throw NPE if a map entry has a null key. Add a null guard.

  2. Lines 83-86: The logic is redundant. isGzipWithQualityAboveZero already returns true only when the encoding is gzip AND quality > 0. The subsequent anyMatch check for equalsIgnoreCase("gzip") is always true for items that passed the filter.

🐛 Proposed fix
     private boolean acceptsGzip(HttpRequest req) {
         String acceptEncoding = req.headers().entrySet().stream()
-            .filter(e -> e.getKey().equalsIgnoreCase("Accept-Encoding"))
+            .filter(e -> e.getKey() != null && e.getKey().equalsIgnoreCase("Accept-Encoding"))
             .map(Map.Entry::getValue)
             .findFirst()
             .orElse(null);

         if (acceptEncoding == null || acceptEncoding.isEmpty()) {
             return false;
         }
         
         return Arrays.stream(acceptEncoding.split(","))
             .map(String::trim)
-            .filter(this::isGzipWithQualityAboveZero)
-            .anyMatch(e -> e.split(";")[0].trim().equalsIgnoreCase("gzip"));
+            .anyMatch(this::isGzipWithQualityAboveZero);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/CompressionFilter.java` around lines 72 - 87,
In acceptsGzip, guard against a null header name by changing the header key
check to ensure e.getKey() != null before calling equalsIgnoreCase, and simplify
the encoding stream by removing the redundant map/filter/anyMatch sequence:
after splitting and trimming the encodings, directly use
anyMatch(this::isGzipWithQualityAboveZero) instead of filtering then checking
equalsIgnoreCase("gzip") because isGzipWithQualityAboveZero already enforces
gzip and quality>0; refer to the acceptsGzip method and
isGzipWithQualityAboveZero when making these changes.

DennSel added 2 commits March 1, 2026 22:13
Check for non-blank Content-Encoding to avoid skipping compression
when header exists but is empty. Prevent duplicate Vary header values
by checking if Accept-Encoding already exists. Use try-with-resources
for GZIPOutputStream to ensure proper resource cleanup.

Fixes:
- Skip compression only if Content-Encoding has actual value
- Avoid duplicate Accept-Encoding in Vary header
- Ensure GZIPOutputStream is closed even on write failure
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/main/java/org/juv25d/filter/CompressionFilter.java (1)

75-90: ⚠️ Potential issue | 🟡 Minor

Potential NPE and redundant filtering logic.

Two issues in acceptsGzip:

  1. Line 77: e.getKey().equalsIgnoreCase(...) will throw NPE if a map entry has a null key. Add a null guard.

  2. Lines 88-89: The logic is redundant. isGzipWithQualityAboveZero already returns true only when the encoding is gzip AND quality > 0. The subsequent anyMatch check for equalsIgnoreCase("gzip") is unnecessary.

🐛 Proposed fix
 private boolean acceptsGzip(HttpRequest req) {
     String acceptEncoding = req.headers().entrySet().stream()
-        .filter(e -> e.getKey().equalsIgnoreCase("Accept-Encoding"))
+        .filter(e -> e.getKey() != null && e.getKey().equalsIgnoreCase("Accept-Encoding"))
         .map(Map.Entry::getValue)
         .findFirst()
         .orElse(null);

     if (acceptEncoding == null || acceptEncoding.isEmpty()) {
         return false;
     }

     return Arrays.stream(acceptEncoding.split(","))
         .map(String::trim)
-        .filter(this::isGzipWithQualityAboveZero)
-        .anyMatch(e -> e.split(";")[0].trim().equalsIgnoreCase("gzip"));
+        .anyMatch(this::isGzipWithQualityAboveZero);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/CompressionFilter.java` around lines 75 - 90,
In acceptsGzip(HttpRequest req) guard against null map keys and simplify the
stream: when filtering headers use a null-safe check (e.getKey() != null &&
e.getKey().equalsIgnoreCase("Accept-Encoding")) or otherwise handle null keys
before equalsIgnoreCase, and replace the final two-stage filter+anyMatch with a
single anyMatch(this::isGzipWithQualityAboveZero) since
isGzipWithQualityAboveZero already checks for "gzip" and quality>0; keep
references to acceptsGzip and isGzipWithQualityAboveZero to locate the changes.
🧹 Nitpick comments (3)
src/main/java/org/juv25d/filter/CompressionFilter.java (3)

18-18: Minor formatting: missing space before {.

Add a space between Filter and { to follow standard Java formatting conventions.

-public class CompressionFilter implements Filter{
+public class CompressionFilter implements Filter {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/CompressionFilter.java` at line 18, The class
declaration "public class CompressionFilter implements Filter{" is missing a
space before the opening brace; update the declaration in CompressionFilter (the
class named CompressionFilter that implements the Filter interface) to "public
class CompressionFilter implements Filter {" to follow standard Java formatting.

109-109: Minor formatting: inconsistent array type spacing.

The array type should use byte[] without spaces for consistency with Java conventions.

-private byte [] compress(byte [] data) throws IOException {
+private byte[] compress(byte[] data) throws IOException {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/CompressionFilter.java` at line 109, In
CompressionFilter.change the method signature for compress to use standard Java
array spacing: replace the inconsistent "byte [] compress(byte [] data)" with
"byte[] compress(byte[] data)" (update both the return type and parameter types
in the compress method declaration and any matching occurrences to maintain
consistency).

97-106: Silent exception handling may cause unintended behavior.

When q= is present but malformed (e.g., gzip;q=invalid), the NumberFormatException is caught and the method falls through to return true (line 106), treating it as acceptable. Consider returning false for malformed quality values instead, or explicitly document this lenient behavior.

             try {
                 double quality = Double.parseDouble(q.substring(2));
                 return quality > 0;
-            } catch (NumberFormatException ignored) {}
+            } catch (NumberFormatException e) {
+                return false; // Malformed quality value, reject
+            }
         }
     }
     return true;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/CompressionFilter.java` around lines 97 -
106, The code in CompressionFilter that parses the "q=" quality (the block
handling parts, q, and parsing into double quality) currently swallows
NumberFormatException and falls through to return true, treating malformed
values as acceptable; change the catch to return false (or explicitly return
false when Double.parseDouble fails) so malformed "q=" values like
"gzip;q=invalid" are treated as unacceptable, or alternatively log the parse
error then return false from the method that contains this parsing logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main/java/org/juv25d/filter/CompressionFilter.java`:
- Around line 75-90: In acceptsGzip(HttpRequest req) guard against null map keys
and simplify the stream: when filtering headers use a null-safe check
(e.getKey() != null && e.getKey().equalsIgnoreCase("Accept-Encoding")) or
otherwise handle null keys before equalsIgnoreCase, and replace the final
two-stage filter+anyMatch with a single
anyMatch(this::isGzipWithQualityAboveZero) since isGzipWithQualityAboveZero
already checks for "gzip" and quality>0; keep references to acceptsGzip and
isGzipWithQualityAboveZero to locate the changes.

---

Nitpick comments:
In `@src/main/java/org/juv25d/filter/CompressionFilter.java`:
- Line 18: The class declaration "public class CompressionFilter implements
Filter{" is missing a space before the opening brace; update the declaration in
CompressionFilter (the class named CompressionFilter that implements the Filter
interface) to "public class CompressionFilter implements Filter {" to follow
standard Java formatting.
- Line 109: In CompressionFilter.change the method signature for compress to use
standard Java array spacing: replace the inconsistent "byte [] compress(byte []
data)" with "byte[] compress(byte[] data)" (update both the return type and
parameter types in the compress method declaration and any matching occurrences
to maintain consistency).
- Around line 97-106: The code in CompressionFilter that parses the "q=" quality
(the block handling parts, q, and parsing into double quality) currently
swallows NumberFormatException and falls through to return true, treating
malformed values as acceptable; change the catch to return false (or explicitly
return false when Double.parseDouble fails) so malformed "q=" values like
"gzip;q=invalid" are treated as unacceptable, or alternatively log the parse
error then return false from the method that contains this parsing logic.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d6df15 and e188d47.

📒 Files selected for processing (1)
  • src/main/java/org/juv25d/filter/CompressionFilter.java

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/main/java/org/juv25d/filter/CompressionFilter.java (1)

75-90: ⚠️ Potential issue | 🟡 Minor

Harden acceptsGzip null-safety and simplify redundant matching.

Line 77 can throw if a header key is null, and Line 88-89 is redundant because isGzipWithQualityAboveZero already enforces gzip with q>0.

🔧 Suggested patch
 private boolean acceptsGzip(HttpRequest req) {
     String acceptEncoding = req.headers().entrySet().stream()
-        .filter(e -> e.getKey().equalsIgnoreCase("Accept-Encoding"))
+        .filter(e -> e.getKey() != null && e.getKey().equalsIgnoreCase("Accept-Encoding"))
         .map(Map.Entry::getValue)
         .findFirst()
         .orElse(null);

     if (acceptEncoding == null || acceptEncoding.isEmpty()) {
         return false;
     }

     return Arrays.stream(acceptEncoding.split(","))
         .map(String::trim)
-        .filter(this::isGzipWithQualityAboveZero)
-        .anyMatch(e -> e.split(";")[0].trim().equalsIgnoreCase("gzip"));
+        .anyMatch(this::isGzipWithQualityAboveZero);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/filter/CompressionFilter.java` around lines 75 - 90,
The acceptsGzip method can NPE when a header key is null and performs a
redundant split/match because isGzipWithQualityAboveZero already ensures the
token is gzip with q>0; update acceptsGzip to safely compare header names (use
"Accept-Encoding".equalsIgnoreCase(key) or guard key != null) when filtering the
headers stream to avoid NPE, and then, after splitting the header value by
commas and trimming, simply call isGzipWithQualityAboveZero and anyMatch on that
result (remove the extra e.split(";")[0] equality check). Keep references to
acceptsGzip and isGzipWithQualityAboveZero when applying these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main/java/org/juv25d/filter/CompressionFilter.java`:
- Around line 75-90: The acceptsGzip method can NPE when a header key is null
and performs a redundant split/match because isGzipWithQualityAboveZero already
ensures the token is gzip with q>0; update acceptsGzip to safely compare header
names (use "Accept-Encoding".equalsIgnoreCase(key) or guard key != null) when
filtering the headers stream to avoid NPE, and then, after splitting the header
value by commas and trimming, simply call isGzipWithQualityAboveZero and
anyMatch on that result (remove the extra e.split(";")[0] equality check). Keep
references to acceptsGzip and isGzipWithQualityAboveZero when applying these
changes.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d6df15 and e188d47.

📒 Files selected for processing (1)
  • src/main/java/org/juv25d/filter/CompressionFilter.java

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/main/java/org/juv25d/util/ConfigLoader.java (1)

13-14: Extract compression defaults into named constants.

1024 and 100 are repeated magic numbers; constants would make this easier to maintain and less error-prone.

♻️ Proposed refactor
 public class ConfigLoader {
     `@Nullable` private static ConfigLoader instance;
+    private static final int DEFAULT_MIN_COMPRESS_SIZE = 1024;
+    private static final int MIN_ALLOWED_COMPRESS_SIZE = 100;
+
     private int port;
     private int minCompressSize;
@@
-            this.minCompressSize = 1024;
+            this.minCompressSize = DEFAULT_MIN_COMPRESS_SIZE;
@@
                 int parsedMinCompressSize =
-                    Integer.parseInt(String.valueOf(compressionConfig.getOrDefault("min-compress-size", 1024)));
-                this.minCompressSize = Math.max(100, parsedMinCompressSize);
+                    Integer.parseInt(
+                        String.valueOf(compressionConfig.getOrDefault("min-compress-size", DEFAULT_MIN_COMPRESS_SIZE))
+                    );
+                this.minCompressSize = Math.max(MIN_ALLOWED_COMPRESS_SIZE, parsedMinCompressSize);
             }

Also applies to: 61-62, 133-135

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/juv25d/util/ConfigLoader.java` around lines 13 - 14,
Extract the magic numbers 1024 and 100 into named constants in ConfigLoader
(e.g., private static final int DEFAULT_MIN_COMPRESS_SIZE = 1024; private static
final int DEFAULT_MAX_COMPRESS_PERCENT = 100) and use these constants to
initialize/compare the existing fields (minCompressSize and whatever field or
parameter uses the 100 percent value) and replace all literal occurrences within
ConfigLoader (including the default field initialization, any constructors, and
validation logic currently using 1024 or 100) so the class consistently
references the new constants (update references in methods that parse or
validate compression settings such as setMinCompressSize / setMaxCompressPercent
or similar).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/java/org/juv25d/util/ConfigLoader.java`:
- Around line 13-14: Extract the magic numbers 1024 and 100 into named constants
in ConfigLoader (e.g., private static final int DEFAULT_MIN_COMPRESS_SIZE =
1024; private static final int DEFAULT_MAX_COMPRESS_PERCENT = 100) and use these
constants to initialize/compare the existing fields (minCompressSize and
whatever field or parameter uses the 100 percent value) and replace all literal
occurrences within ConfigLoader (including the default field initialization, any
constructors, and validation logic currently using 1024 or 100) so the class
consistently references the new constants (update references in methods that
parse or validate compression settings such as setMinCompressSize /
setMaxCompressPercent or similar).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5d6df15 and 1b862fa.

📒 Files selected for processing (3)
  • src/main/java/org/juv25d/filter/CompressionFilter.java
  • src/main/java/org/juv25d/util/ConfigLoader.java
  • src/main/resources/application-properties.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/juv25d/filter/CompressionFilter.java

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add GZIP compression filter for response bodies

3 participants