11package org .example .filter ;
22
3+ import com .aayushatharva .brotli4j .Brotli4jLoader ;
34import org .example .http .HttpResponseBuilder ;
45import org .example .httpparser .HttpRequest ;
6+ import com .aayushatharva .brotli4j .encoder .Encoder ;
57
68import java .io .ByteArrayOutputStream ;
79import java .io .IOException ;
1012import java .util .zip .GZIPOutputStream ;
1113
1214/**
13- * Compression filter that compresses HTTP responses with gzip when supported by client.
15+ * Compression filter that compresses HTTP responses with gzip or Brotli when supported by client.
1416 *
15- * <p>This filter applies gzip compression to HTTP responses when the following conditions are met:
17+ * <p>This filter applies compression to HTTP responses when the following conditions are met:
1618 * <ul>
17- * <li>Client sends Accept-Encoding header containing "gzip"</li>
19+ * <li>Client sends Accept-Encoding header containing "gzip" or "br" (Brotli) </li>
1820 * <li>Response body is larger than 1KB (MIN_COMPRESS_SIZE)</li>
1921 * <li>Content-Type is compressible (text-based formats like HTML, CSS, JS, JSON)</li>
2022 * <li>Content-Type is not already compressed (images, videos, zip files)</li>
2123 * </ul>
2224 *
25+ * <p>Compression priority when client supports multiple algorithms:
26+ * <ol>
27+ * <li>Brotli (br) - Best compression ratio</li>
28+ * <li>Gzip - Fallback if Brotli fails or not accepted</li>
29+ * </ol>
30+ *
2331 * <p>When compression is applied, the filter:
2432 * <ul>
25- * <li>Compresses the response body using gzip </li>
26- * <li>Sets Content-Encoding: gzip header</li>
33+ * <li>Compresses the response body using the best available algorithm </li>
34+ * <li>Sets Content-Encoding header (br or gzip) </li>
2735 * <li>Sets Vary: Accept-Encoding header for proper caching</li>
2836 * </ul>
2937 *
3038 */
3139
3240public class CompressionFilter implements Filter {
3341 private static final int MIN_COMPRESS_SIZE = 1024 ;
42+ private static final boolean BROTLI_AVAILABLE ;
43+
44+ static {
45+ boolean available ;
46+ try {
47+ Brotli4jLoader .ensureAvailability ();
48+ available = true ;
49+ } catch (Exception e ) {
50+ available = false ;
51+ System .err .println ("Brotli native library not available: " + e .getMessage ());
52+ }
53+ BROTLI_AVAILABLE = available ;
54+ }
3455
3556 private static final Set <String > COMPRESSIBLE_TYPES = Set .of (
3657 "text/html" ,
@@ -67,7 +88,7 @@ private void compressIfNeeded(HttpRequest request, HttpResponseBuilder response)
6788 }
6889
6990 String acceptEncoding = getHeader (request , "Accept-Encoding" );
70- if (acceptEncoding == null || ! acceptEncoding . toLowerCase (). contains ( "gzip" ) ) {
91+ if (acceptEncoding == null ) {
7192 return ;
7293 }
7394
@@ -81,23 +102,72 @@ private void compressIfNeeded(HttpRequest request, HttpResponseBuilder response)
81102 return ;
82103 }
83104
105+ if (BROTLI_AVAILABLE && isEncodingAccepted (acceptEncoding , "br" )) {
106+ if (tryBrotliCompression (response , originalBody )) {
107+ return ;
108+ }
109+ }
110+
111+ if (isEncodingAccepted (acceptEncoding , "gzip" )) {
112+ tryGzipCompression (response , originalBody );
113+ }
114+ }
115+
116+ private boolean isEncodingAccepted (String acceptEncoding , String encoding ) {
117+ String [] parts = acceptEncoding .toLowerCase ().split ("," );
118+
119+ for (String part : parts ) {
120+ part = part .trim ();
121+
122+ if (part .startsWith (encoding )) {
123+ if (part .contains ("q=0" )) {
124+ return false ;
125+ }
126+ return true ;
127+ }
128+ }
129+
130+ return false ;
131+ }
132+
133+ private boolean tryBrotliCompression (HttpResponseBuilder response , byte [] originalBody ) {
134+ try {
135+ byte [] compressed = brotliCompress (originalBody );
136+
137+ response .setBody (compressed );
138+ response .setHeader ("Content-Encoding" , "br" );
139+ response .removeHeader ("Content-Length" );
140+ updateVaryHeader (response );
141+
142+ return true ;
143+ } catch (IOException e ) {
144+ System .err .println ("Brotli compression failed, falling back to gzip: " + e .getMessage ());
145+ return false ;
146+ }
147+ }
148+
149+ private void tryGzipCompression (HttpResponseBuilder response , byte [] originalBody ) {
84150 try {
85151 byte [] compressed = gzipCompress (originalBody );
86152
87153 response .setBody (compressed );
88154 response .setHeader ("Content-Encoding" , "gzip" );
89-
90- String existingVary = response .getHeader ("Vary" );
91- if (existingVary != null && !existingVary .isEmpty ()) {
92- if (!existingVary .toLowerCase ().contains ("accept-encoding" )) {
93- response .setHeader ("Vary" , existingVary + ", Accept-Encoding" );
94- }
95- } else {
96- response .setHeader ("Vary" , "Accept-Encoding" );
97- }
155+ response .removeHeader ("Content-Length" );
156+ updateVaryHeader (response );
98157
99158 } catch (IOException e ) {
100- System .err .println ("CompressionFilter: gzip compression failed: " + e .getMessage ());
159+ System .err .println ("Gzip compression failed: " + e .getMessage ());
160+ }
161+ }
162+
163+ private void updateVaryHeader (HttpResponseBuilder response ) {
164+ String existingVary = response .getHeader ("Vary" );
165+ if (existingVary != null && !existingVary .isEmpty ()) {
166+ if (!existingVary .toLowerCase ().contains ("accept-encoding" )) {
167+ response .setHeader ("Vary" , existingVary + ", Accept-Encoding" );
168+ }
169+ } else {
170+ response .setHeader ("Vary" , "Accept-Encoding" );
101171 }
102172 }
103173
@@ -140,6 +210,14 @@ private byte[] gzipCompress(byte[] data) throws IOException {
140210 return byteStream .toByteArray ();
141211 }
142212
213+ private byte [] brotliCompress (byte [] data ) throws IOException {
214+ try {
215+ return Encoder .compress (data );
216+ } catch (Exception e ) {
217+ throw new IOException ("Brotli compression failed" , e );
218+ }
219+ }
220+
143221 @ Override
144222 public void destroy () {
145223 }
0 commit comments