diff --git a/incubator/brotli/pom.xml b/incubator/brotli/pom.xml
new file mode 100644
index 00000000000..a2a448b4e10
--- /dev/null
+++ b/incubator/brotli/pom.xml
@@ -0,0 +1,53 @@
+
+
+ 4.0.0
+ 2.38-SNAPSHOT
+
+ project
+ org.glassfish.jersey
+ 2.38-SNAPSHOT
+ ../../pom.xml
+
+
+ org.glassfish.jersey.message
+ jersey-brotli
+ jar
+ brotli
+
+
+
+
+ org.glassfish.jersey.core
+ jersey-common
+ ${project.version}
+
+
+ com.oracle.brotli
+ brotli
+ 1.0.0-SNAPSHOT
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+
+
+ org.glassfish.jersey.test-framework.providers
+ jersey-test-framework-provider-external
+ ${project.version}
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter
+ test
+
+
+ org.hamcrest
+ hamcrest
+ test
+
+
+
+
\ No newline at end of file
diff --git a/incubator/brotli/src/main/java/org/glassfish/jersey/message/BrotliEncoder.java b/incubator/brotli/src/main/java/org/glassfish/jersey/message/BrotliEncoder.java
new file mode 100644
index 00000000000..92083a60887
--- /dev/null
+++ b/incubator/brotli/src/main/java/org/glassfish/jersey/message/BrotliEncoder.java
@@ -0,0 +1,37 @@
+package org.glassfish.jersey.message;
+
+import com.oracle.brotli.decoder.BrotliInputStream;
+import com.oracle.brotli.encoder.BrotliOutputStream;
+import org.glassfish.jersey.spi.ContentEncoder;
+
+import javax.annotation.Priority;
+import javax.ws.rs.Priorities;
+import javax.ws.rs.core.HttpHeaders;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * Brotli encoding support. Interceptor that encodes the output or decodes the input if
+ * {@link HttpHeaders#CONTENT_ENCODING Content-Encoding header} value equals to {@code br}.
+ */
+@Priority(Priorities.ENTITY_CODER)
+public class BrotliEncoder extends ContentEncoder {
+
+ /**
+ * Initialize BrotliEncoder.
+ */
+ public BrotliEncoder() {
+ super("br");
+ }
+
+ @Override
+ public InputStream decode(String contentEncoding, InputStream encodedStream) throws IOException {
+ return BrotliInputStream.builder().inputStream(encodedStream).build();
+ }
+
+ @Override
+ public OutputStream encode(String contentEncoding, OutputStream entityStream) throws IOException {
+ return BrotliOutputStream.builder().outputStream(entityStream).build();
+ }
+}
diff --git a/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliEncoderTest.java b/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliEncoderTest.java
new file mode 100644
index 00000000000..3974a21f555
--- /dev/null
+++ b/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliEncoderTest.java
@@ -0,0 +1,98 @@
+package org.glassfish.jersey.message;
+
+import com.oracle.brotli.decoder.BrotliInputStream;
+import com.oracle.brotli.encoder.BrotliOutputStream;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class BrotliEncoderTest {
+
+ @Test
+ public void testEncode() throws IOException {
+ test(new TestSpec() {
+ @Override
+ public OutputStream getEncoded(OutputStream stream) throws IOException {
+ return new BrotliEncoder().encode("br", stream);
+ }
+
+ @Override
+ public InputStream getDecoded(InputStream stream) throws IOException {
+ return BrotliInputStream.builder().inputStream(stream).build();
+ }
+ });
+ }
+
+ @Test
+ public void testDecode() throws IOException {
+ test(new TestSpec() {
+ @Override
+ public OutputStream getEncoded(OutputStream stream) throws IOException {
+ return BrotliOutputStream.builder().outputStream(stream).build();
+ }
+
+ @Override
+ public InputStream getDecoded(InputStream stream) throws IOException {
+ return new BrotliEncoder().decode("br", stream);
+ }
+ });
+ }
+
+ @Test
+ public void testEncodeDecode() throws IOException {
+ test(new TestSpec() {
+ @Override
+ public OutputStream getEncoded(OutputStream stream) throws IOException {
+ return new BrotliEncoder().encode("br", stream);
+ }
+
+ @Override
+ public InputStream getDecoded(InputStream stream) throws IOException {
+ return new BrotliEncoder().decode("br", stream);
+ }
+ });
+ }
+
+ void test(TestSpec testSpec) throws IOException {
+ byte[] entity = "Hello world!".getBytes();
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ OutputStream encoded = testSpec.getEncoded(baos);
+ encoded.write(entity);
+ encoded.close();
+ ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
+ byte[] result = new byte[entity.length];
+ InputStream decoded = testSpec.getDecoded(bais);
+ int len = decoded.read(result);
+ assertEquals(-1, decoded.read());
+ decoded.close();
+ assertEquals(entity.length, len);
+ assertArrayEquals(entity, result);
+ }
+
+ interface TestSpec {
+ /**
+ * Returns encoded stream.
+ *
+ * @param stream Original stream.
+ * @return Encoded stream.
+ * @throws IOException I/O exception.
+ */
+ OutputStream getEncoded(OutputStream stream) throws IOException;
+
+ /**
+ * Returns decoded stream.
+ *
+ * @param stream Original stream.
+ * @return Decoded stream.
+ * @throws IOException I/O exception.
+ */
+ InputStream getDecoded(InputStream stream) throws IOException;
+ }
+}
diff --git a/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliITTest.java b/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliITTest.java
new file mode 100644
index 00000000000..15731c028d0
--- /dev/null
+++ b/incubator/brotli/src/test/java/org/glassfish/jersey/message/BrotliITTest.java
@@ -0,0 +1,95 @@
+package org.glassfish.jersey.message;
+
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.server.filter.EncodingFilter;
+import org.glassfish.jersey.test.JerseyTest;
+import org.glassfish.jersey.test.TestProperties;
+import org.glassfish.jersey.test.external.ExternalTestContainerFactory;
+import org.glassfish.jersey.test.spi.TestContainerException;
+import org.glassfish.jersey.test.spi.TestContainerFactory;
+import org.junit.jupiter.api.Test;
+
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Response;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+public class BrotliITTest extends JerseyTest {
+
+ private static final String CONTENT_ENCODING = "Content-Encoding";
+ private static final String BR = "br";
+
+ @Override
+ protected Application configure() {
+ enable(TestProperties.LOG_TRAFFIC);
+ enable(TestProperties.DUMP_ENTITY);
+ return new MyApplication();
+ }
+
+ @Override
+ protected TestContainerFactory getTestContainerFactory() throws TestContainerException {
+ return new ExternalTestContainerFactory();
+ }
+
+ protected void assertHtmlResponse(final String response) {
+ assertNotNull(response, "No text returned!");
+
+ assertResponseContains(response, "");
+ assertResponseContains(response, "");
+ }
+
+ protected void assertResponseContains(final String response, final String text) {
+ assertTrue(response.contains(text), "Response should contain " + text + " but was: " + response);
+ }
+
+ @Test
+ public void testString() throws Exception {
+ Response response = target("/client/string")
+ .register(BrotliEncoder.class)
+ .request("text/html")
+ .acceptEncoding(BR)
+ .get();
+ String resp = response.readEntity(String.class);
+ assertResponseContains(resp, "string string string string string string");
+ assertEquals(BR, response.getHeaderString(CONTENT_ENCODING));
+ }
+
+ @Test
+ public void testJsp() throws Exception {
+ Response response = target("/client/html")
+ .register(BrotliEncoder.class)
+ .request("text/html", "application/xhtml+xml", "application/xml;q=0.9", "*/*;q=0.8")
+ .acceptEncoding(BR)
+ .get();
+ String resp = response.readEntity(String.class);
+ assertHtmlResponse(resp);
+ assertResponseContains(resp, "find this string");
+ assertEquals(BR, response.getHeaderString(CONTENT_ENCODING));
+ }
+
+ @Test
+ public void testJspNotDecoded() throws Exception {
+ Response response = target("/client/html")
+ .request("text/html", "application/xhtml+xml", "application/xml;q=0.9", "*/*;q=0.8")
+ .acceptEncoding(BR)
+ .get();
+ String resp = response.readEntity(String.class);
+ assertFalse(resp.contains("find this string"));
+ assertEquals(BR, response.getHeaderString(CONTENT_ENCODING));
+ }
+
+ class MyApplication extends ResourceConfig {
+
+ public MyApplication() {
+ property("jersey.config.server.mvc.templateBasePath.jsp", "/WEB-INF/jsp");
+ property("jersey.config.servlet.filter.forwardOn404", "true");
+ property("jersey.config.servlet.filter.staticContentRegex", "/WEB-INF/.*\\.jsp");
+ packages(MyApplication.class.getPackage().getName());
+ EncodingFilter.enableFor(this, new Class[] {BrotliEncoder.class});
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/incubator/pom.xml b/incubator/pom.xml
index 1a362714fac..98566634fed 100644
--- a/incubator/pom.xml
+++ b/incubator/pom.xml
@@ -42,6 +42,7 @@
html-json
kryo
open-tracing
+ brotli