ballerina-platform / ballerina-library

The Ballerina Library
https://ballerina.io/learn/api-docs/ballerina/
Apache License 2.0
136 stars 64 forks source link

Add HTTP compression support for `brotli` format #6547

Open TharmiganK opened 6 months ago

TharmiganK commented 6 months ago

Description:

There is a requirement to add support for brotli format in the HTTP response compression/decompression. Currently the HTTP package only supports deflate and gzip. Please note that netty already supports this format, but we need to pack some native libraries to enable the compression/decompression.

Describe your problem(s)

The current HTTP client does not support decompressing response payload which is compressed with brotli compression format. In addition to that, the HTTP server does not support the brotli compression for sending response payloads.

This was checked using the following backend written in expressjs:

const express = require('express');
const compression = require('compression');
const zlib = require('zlib');

const app = express();

// Use compression middleware with Brotli encoding
app.use(compression({
  brotli: {
    enabled: true,
    zlib: {
      // options for zlib.BrotliOptions
      [zlib.constants.BROTLI_PARAM_QUALITY]: 4,
    },
  },
}));

// Middleware to set Content-Encoding to br
app.use((req, res, next) => {
  res.setHeader('Content-Encoding', 'br');
  res.setHeader('Content-Type', 'application/xml');
  next();
});

app.get('/mock', (req, res) => {
  const xmlData = `
    <response>
      <message>This is a mocked XML response</message>
    </response>
  `;

  // Compress the XML response using Brotli
  zlib.brotliCompress(Buffer.from(xmlData), (err, result) => {
    if (err) {
      res.status(500).send('Error compressing data');
    } else {
      res.send(result);
    }
  });
});

const PORT = 3000;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

The endpoint: localhost:3000/mock returns an xml payload compressed in brotli format.

Use the following ballerina service to check the behavior:

import ballerina/http;
import ballerina/xmldata;

// Endpoint returning a Brotli compressed payload
http:Client albumClient = check new ("localhost:3000");

service / on new http:Listener(9090) {

    // Checking the passthrough scenario, to just get the response from backend
    resource function get passthrough(http:Request req) returns http:Response|error {
        http:Response resp = check albumClient->forward("/mock", req);
        resp.addHeader("Test-Encoding", "Brotli");
        return resp;
    }

    // Checking the payload extraction scenario with passthrough response
    resource function get passthroughJson(http:Request req) returns http:Response|error {
        http:Response resp = check albumClient->forward("/mock", req);
        xml payload = check resp.getXmlPayload();
        json jsonValue = check xmldata:toJson(payload);
        resp.addHeader("Test-Encoding", "Brotli");
        resp.setPayload(jsonValue);
        return resp;
    }

    // Checking the simple scenario, where we return a response with `Content-Encoding: br` header 
    resource function get test() returns http:Response {
        xml payload = xml `<response><message>This is a mocked XML response</message></response>`;
        http:Response res = new;
        res.setPayload(payload);
        res.setHeader("content-encoding", "br");
        return res;
    }
}
Find the observation as below: (Every request to these 3 resources should have Accept-Encoding: gzip, deflate, br header) Scenario Observation Expected behavior
just passthrough Returns the raw response payload without Content-Encoding header Should returns the raw response payload with Content-Encoding: br header
passthrough with payload extraction Returns a payload binding error since we are trying to build the payload without decompression Should return a success response, and the payload extraction should work without failures
simple response with Content-Encoding: br header Returns the normal response payload without Content-Encoding header Should return a payload compressed in brotli format with the Content-Encoding: br header

Describe your solution(s)

Add the compression and decompression support for the brotli format in the service and the client. This requires the addition of the following brotli4j compression/decompression library which is used by netty:

TharmiganK commented 3 months ago

Currently checking an error in HTTP/2 client when retrieving the response payload which is compressed using brotli format. Getting a poll time out when trying to get the http content. Reproducible with the following:

int http2AcceptEncodingHeaderTestPort = 9090;

listener http:Listener http2AcceptEncodingListenerEP = new (http2AcceptEncodingHeaderTestPort, server = "Mysql" );

service /hello on http2AcceptEncodingListenerEP {

resource function 'default .(http:Caller caller, http:Request req) returns error? {
    http:Response res = new;
    map<json> payload = {};
    boolean hasHeader = req.hasHeader("Accept-Encoding");
    if hasHeader {
        log:printInfo("Accept-Encoding header is present.");
        payload["acceptEncoding"] = check req.getHeader("Accept-Encoding");
    } else {
        log:printInfo("Accept-Encoding header is not present.");
        payload["acceptEncoding"] = "Not present";
    }
    log:printInfo("Sending response.");
    res.setJsonPayload(payload);
    check caller->respond(res);
}

}


- client:
```bal
import ballerina/http;
import ballerina/log;

int http2AcceptEncodingHeaderTestPort = 9090;

final http:Client http2AcceptEncodingEnableEP = check new (
    "http://localhost:" + http2AcceptEncodingHeaderTestPort.toString() + "/hello",
    http2Settings = {
        http2PriorKnowledge: true
    },
    compression = http:COMPRESSION_ALWAYS
);

public function main() returns error? {
    http:Request req = new;
    req.setTextPayload("accept encoding test");
    log:printInfo("Sending request.");

    json|error response = http2AcceptEncodingEnableEP->post("/", req);
    log:printInfo("Response received.");
    if response is json {
        log:printInfo("response recieved", payload = response);
    } else {
        log:printError("error occured", response);
    }
}