Skip to content

Add Brotli(br) content decoding support#2572

Merged
SanojPunchihewa merged 2 commits into
wso2:masterfrom
SanojPunchihewa:brotli
Jun 26, 2026
Merged

Add Brotli(br) content decoding support#2572
SanojPunchihewa merged 2 commits into
wso2:masterfrom
SanojPunchihewa:brotli

Conversation

@SanojPunchihewa

@SanojPunchihewa SanojPunchihewa commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Purpose

Add Brotli(br) content decoding support

Fixes: wso2/product-integrator-mi#4037

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Overview

This pull request adds Brotli (Content-Encoding: br) content decoding support to the WSO2 HTTP Connector so Brotli-encoded HTTP responses are transparently decompressed before downstream message parsing.

Changes

  • Dependency updates
    • Added the Orbit-packaged commons-compress dependency to support Brotli decompression.
    • Updated the root pom.xml to introduce a commons-compress.orbit.version property and corresponding dependency management entry.
  • Response/body decompression logic
    • Updated DeferredMessageBuilder to route incoming request-body/response streams through a new handleContentEncoding(...) method.
    • Implemented handling for Content-Encoding: br by creating a BrotliCompressorInputStream (with a small stream probe via PushbackInputStream to avoid issues on empty streams).
    • For non-Brotli encodings, it delegates to the existing gzip handling path (HTTPTransportUtils.handleGZip(...)).
    • Added logic to match content-encoding values against both standard and lowercase header variants.

Impact

Fixes HTTP Connector message processing failures caused by Brotli-compressed payloads being treated as if they were uncompressed (previously leading to downstream JSON parsing errors and misleading “not a JSON string” style failures). The connector now correctly decodes Brotli-encoded content prior to parsing.

Walkthrough

The changes add Brotli decompression support to the nhttp transport module. A new Maven dependency on org.wso2.orbit.org.apache.commons:commons-compress is added to pom.xml to provide the Brotli decompressor. In DeferredMessageBuilder, the getDocument(...) method is updated to replace the unconditional GZip handling with a call to a new handleContentEncoding(...) dispatcher. This dispatcher inspects the Content-Encoding header and routes to BrotliCompressorInputStream (with a PushbackInputStream probe) for Brotli, to HTTPTransportUtils.handleGZip(...) for GZip, or passes the stream through unchanged for other encodings.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete. While it identifies the purpose and linked issue, it lacks critical sections including goals, approach, user stories, release notes, documentation, testing details, and security checks required by the template. Complete the pull request description by adding missing sections: Goals, Approach, User Stories, Release Note, Documentation, Testing (unit and integration), Security Checks, and Test Environment from the template.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the primary change: adding Brotli (br) content decoding support to the HTTP transport layer.
Linked Issues check ✅ Passed The pull request implementation aligns with issue #4037 objectives: it adds Brotli decompression support via a new handleContentEncoding() method that properly routes Brotli-encoded streams, fixing parsing failures.
Out of Scope Changes check ✅ Passed All changes are directly scoped to implementing Brotli decompression support. The Maven dependency additions, DeferredMessageBuilder routing logic, and pom.xml configuration changes are all necessary for the stated objective.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PMD (7.25.0)
pom.xml

[ERROR] Cannot load ruleset .coderabbit-pmd-ruleset.xml: Cannot resolve rule/ruleset reference '.coderabbit-pmd-ruleset.xml'. Make sure the resource is a valid file or URL and is on the CLASSPATH. Use --debug (or a fine log level) to see the current classpath.
[WARN] Progressbar rendering conflicts with reporting to STDOUT. No progressbar will be shown. Try running with argument -r to output the report to a file instead.

modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/util/DeferredMessageBuilder.java

[ERROR] Cannot load ruleset .coderabbit-pmd-ruleset.xml: Cannot resolve rule/ruleset reference '.coderabbit-pmd-ruleset.xml'. Make sure the resource is a valid file or URL and is on the CLASSPATH. Use --debug (or a fine log level) to see the current classpath.
[WARN] Progressbar rendering conflicts with reporting to STDOUT. No progressbar will be shown. Try running with argument -r to output the report to a file instead.


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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@modules/transports/core/nhttp/pom.xml`:
- Around line 233-236: The commons-compress dependency in the nhttp pom.xml is
missing a version tag and is not declared in the project's dependencyManagement
section, which will cause Maven resolution to fail. Add a version tag directly
to the commons-compress dependency element, or alternatively define it in the
root pom.xml's dependencyManagement section with the appropriate version and
then reference it from the nhttp module without the version tag. Additionally,
verify that the WSO2 orbit commons-compress artifact version being used includes
Brotli decoder support, and if it does not, add an explicit Brotli decoder
dependency to ensure runtime support.

In
`@modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/util/DeferredMessageBuilder.java`:
- Around line 326-330: The checkContentEncodingMatches method performs
case-sensitive string comparisons on HTTP Content-Encoding header values, which
violates HTTP specifications where content-coding tokens are case-insensitive.
Modify the method to normalize both the encoding parameter and the header value
retrieved from headers.get(HTTPConstants.HEADER_CONTENT_ENCODING) and
headers.get(HTTPConstants.HEADER_CONTENT_ENCODING_LOWERCASE) to lowercase and
trim any whitespace before performing the equality comparisons, allowing values
like BR, Br, or GZIP to correctly match their lowercase equivalents.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 68688a5b-070f-4847-a0a0-62cdbf571c7c

📥 Commits

Reviewing files that changed from the base of the PR and between 67ee70a and b2db77c.

📒 Files selected for processing (2)
  • modules/transports/core/nhttp/pom.xml
  • modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/util/DeferredMessageBuilder.java

Comment thread modules/transports/core/nhttp/pom.xml
Comment on lines +326 to +330
private static boolean checkContentEncodingMatches(MessageContext msgContext, String encoding) {
Map headers = (Map)msgContext.getProperty(MessageContext.TRANSPORT_HEADERS);
return headers != null && (encoding.equals(headers.get(HTTPConstants.HEADER_CONTENT_ENCODING))
|| encoding.equals(headers.get(HTTPConstants.HEADER_CONTENT_ENCODING_LOWERCASE)));
}

@coderabbitai coderabbitai Bot Jun 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Match the Content-Encoding value case-insensitively.

checkContentEncodingMatches compares the header value with encoding.equals(...), which is case-sensitive. HTTP content-coding tokens are case-insensitive, so values such as BR, Br, or GZIP would not match and the body would be passed through undecoded, leading to a build/parse failure downstream. Consider normalizing the header value (e.g., trim + equalsIgnoreCase) before comparison.

♻️ Proposed adjustment
     private static boolean checkContentEncodingMatches(MessageContext msgContext, String encoding) {
         Map headers = (Map)msgContext.getProperty(MessageContext.TRANSPORT_HEADERS);
-        return headers != null && (encoding.equals(headers.get(HTTPConstants.HEADER_CONTENT_ENCODING))
-                || encoding.equals(headers.get(HTTPConstants.HEADER_CONTENT_ENCODING_LOWERCASE)));
+        if (headers == null) {
+            return false;
+        }
+        Object value = headers.get(HTTPConstants.HEADER_CONTENT_ENCODING);
+        if (value == null) {
+            value = headers.get(HTTPConstants.HEADER_CONTENT_ENCODING_LOWERCASE);
+        }
+        return value instanceof String && encoding.equalsIgnoreCase(((String) value).trim());
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private static boolean checkContentEncodingMatches(MessageContext msgContext, String encoding) {
Map headers = (Map)msgContext.getProperty(MessageContext.TRANSPORT_HEADERS);
return headers != null && (encoding.equals(headers.get(HTTPConstants.HEADER_CONTENT_ENCODING))
|| encoding.equals(headers.get(HTTPConstants.HEADER_CONTENT_ENCODING_LOWERCASE)));
}
private static boolean checkContentEncodingMatches(MessageContext msgContext, String encoding) {
Map headers = (Map)msgContext.getProperty(MessageContext.TRANSPORT_HEADERS);
if (headers == null) {
return false;
}
Object value = headers.get(HTTPConstants.HEADER_CONTENT_ENCODING);
if (value == null) {
value = headers.get(HTTPConstants.HEADER_CONTENT_ENCODING_LOWERCASE);
}
return value instanceof String && encoding.equalsIgnoreCase(((String) value).trim());
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/util/DeferredMessageBuilder.java`
around lines 326 - 330, The checkContentEncodingMatches method performs
case-sensitive string comparisons on HTTP Content-Encoding header values, which
violates HTTP specifications where content-coding tokens are case-insensitive.
Modify the method to normalize both the encoding parameter and the header value
retrieved from headers.get(HTTPConstants.HEADER_CONTENT_ENCODING) and
headers.get(HTTPConstants.HEADER_CONTENT_ENCODING_LOWERCASE) to lowercase and
trim any whitespace before performing the equality comparisons, allowing values
like BR, Br, or GZIP to correctly match their lowercase equivalents.

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.

This is a behavioural change and may break existing flows.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@SanojPunchihewa — The concern is understandable, but this particular direction of change is strictly additive. equalsIgnoreCase is a superset of equals: every value that matches today (e.g., br, gzip) will continue to match after the change. The only new matches would be mixed-case variants (BR, GZIP, Br, etc.) that currently fall through undecoded. No currently working flow would be broken because no currently matching value would stop matching.

Per RFC 7231 §3.1.2.2, content-coding token comparisons are specified as case-insensitive, so the current case-sensitive behavior is technically a spec deviation rather than an intentional design choice. That said, if you'd prefer to keep the scope of this PR minimal and address case normalization separately, that's also a reasonable call.

Comment thread pom.xml Outdated
<org.wso2.orbit.atlassian.oai.version>2.44.1.wso2v1</org.wso2.orbit.atlassian.oai.version>
<swagger.parser.v3.version>2.1.37.wso2v1</swagger.parser.v3.version>
<jaxen.version>2.0.0</jaxen.version>
<commons-compress.orbit.version>1.28.0.wso2v1</commons-compress.orbit.version>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

isn;t this v2?

} else if (checkContentEncodingMatches(msgContext, "gzip")) {
return HTTPTransportUtils.handleGZip(msgContext, in);
}
return in;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is different from the old behaviour. Can this break existing usecases?

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.

HTTPTransportUtils.handleGZip returns the original input stream unchanged when the Content-Encoding header does not match gzip encoding. Therefore, this change does not affect the existing behaviour

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Claude comment: "The original code unconditionally called HTTPTransportUtils.handleGZip(msgCtx, in), which checks the Content-Encoding header internally and handles both gzip and x-gzip (a legacy alias used by some servers). The new code calls handleGZip only
when checkContentEncodingMatches(msgContext, "gzip") is true. If a server sends Content-Encoding: x-gzip, the new check misses it and returns the compressed stream undecoded."

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.

Following is the current code which only checks gzip.

public static InputStream handleGZip(MessageContext msgContext, InputStream in)
            throws IOException {
        Map headers = (Map) msgContext.getProperty(MessageContext.TRANSPORT_HEADERS);

        if (headers != null) {
            if (HTTPConstants.COMPRESSION_GZIP
                    .equals(headers.get(HTTPConstants.HEADER_CONTENT_ENCODING)) ||
                    HTTPConstants.COMPRESSION_GZIP.equals(headers.get(
                            HTTPConstants.HEADER_CONTENT_ENCODING_LOWERCASE))) {
                PushbackInputStream pushbackInputStream = new PushbackInputStream(in);
                int bytesRead = pushbackInputStream.read();
                if (bytesRead != -1) {
                    pushbackInputStream.unread(bytesRead);
                    return new GZIPInputStream(pushbackInputStream);
                }
            }
        }
        return in;
    }

Bump Commons compress to 1.28.0.wso2v2

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/util/DeferredMessageBuilder.java (1)

312-322: 🩺 Stability & Availability | 🟠 Major

Add the Brotli decoder runtime dependency to the classpath.

BrotliCompressorInputStream from commons-compress requires the external org.brotli:dec library to perform decompression. This library is not bundled by commons-compress and is not currently declared in the project dependencies. When a Content-Encoding: br response is processed, instantiation at line 318 will fail at runtime (e.g., NoClassDefFoundError or CompressorException). Declare org.brotli:dec as a dependency in the nhttp module to ensure Brotli-compressed responses are handled correctly in production.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/util/DeferredMessageBuilder.java`
around lines 312 - 322, The method handleContentEncoding instantiates
BrotliCompressorInputStream which requires the org.brotli:dec library for
decompression, but this dependency is not currently declared in the project. Add
the org.brotli:dec library as a runtime dependency in the nhttp module's
dependency configuration (typically in pom.xml) to ensure the Brotli
decompression functionality works correctly in production without experiencing
ClassNotFoundError or CompressorException failures.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/util/DeferredMessageBuilder.java`:
- Around line 312-322: The method handleContentEncoding instantiates
BrotliCompressorInputStream which requires the org.brotli:dec library for
decompression, but this dependency is not currently declared in the project. Add
the org.brotli:dec library as a runtime dependency in the nhttp module's
dependency configuration (typically in pom.xml) to ensure the Brotli
decompression functionality works correctly in production without experiencing
ClassNotFoundError or CompressorException failures.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 23c5b29c-a8e4-4f37-8df0-2d6c30f7f27f

📥 Commits

Reviewing files that changed from the base of the PR and between eeabd5d and ee9d450.

📒 Files selected for processing (2)
  • modules/transports/core/nhttp/src/main/java/org/apache/synapse/transport/passthru/util/DeferredMessageBuilder.java
  • pom.xml
🚧 Files skipped from review as they are similar to previous changes (1)
  • pom.xml

@SanojPunchihewa SanojPunchihewa merged commit 32499aa into wso2:master Jun 26, 2026
2 of 3 checks passed
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.

HTTP Connector Fails to Handle Brotli-Encoded Responses

2 participants