Skip to content

Commit 3b579ea

Browse files
committed
merge try
2 parents cce901b + 5280790 commit 3b579ea

12 files changed

Lines changed: 366 additions & 178 deletions

File tree

pom.xml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@
7171
<version>1.20.0</version>
7272
</dependency>
7373

74+
<dependency>
75+
<groupId>ch.qos.logback</groupId>
76+
<artifactId>logback-classic</artifactId>
77+
<version>1.5.32</version>
78+
<scope>compile</scope>
79+
</dependency>
80+
7481
</dependencies>
7582
<build>
7683
<plugins>

src/main/java/org/example/TcpServer.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
package org.example;
22

33
import org.example.http.HttpResponseBuilder;
4+
import org.slf4j.Logger;
5+
import org.slf4j.LoggerFactory;
46

57
import java.io.IOException;
68
import java.io.OutputStream;
7-
import java.io.PrintWriter;
89
import java.net.ServerSocket;
910
import java.net.Socket;
10-
import java.nio.charset.StandardCharsets;
1111
import java.util.Map;
1212

1313
public class TcpServer {
1414

1515
private final int port;
1616
private final ConnectionFactory connectionFactory;
17+
private static final Logger log = LoggerFactory.getLogger(TcpServer.class);
1718

1819
public TcpServer(int port, ConnectionFactory connectionFactory) {
1920
this.port = port;
2021
this.connectionFactory = connectionFactory;
2122
}
2223

2324
public void start() {
24-
System.out.println("Starting TCP server on port " + port);
25+
log.info("Starting TCP server on port {}", port);
2526

2627
try (ServerSocket serverSocket = new ServerSocket(port)) {
2728
while (true) {
2829
Socket clientSocket = serverSocket.accept(); // block
29-
System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
30+
log.info("Client connected");
3031
Thread.ofVirtual().start(() -> handleClient(clientSocket));
3132
}
3233
} catch (IOException e) {
@@ -38,16 +39,22 @@ protected void handleClient(Socket client) {
3839
try(client){
3940
processRequest(client);
4041
} catch (Exception e) {
41-
throw new RuntimeException("Failed to close socket", e);
42+
log.error("Failed when handling connection to client", e);
4243
}
4344
}
4445

46+
/*
47+
The connection handler must be run in a try-catch-finally. Otherwise - if we use try-with-resources - the
48+
connection will always be closed when code reaches handleInternalServerError() and there will never
49+
be an error print on the client's webpage.
50+
*/
4551
private void processRequest(Socket client) throws Exception {
4652
ConnectionHandler handler = null;
4753
try{
4854
handler = connectionFactory.create(client);
4955
handler.runConnectionHandler();
5056
} catch (Exception e) {
57+
log.error("Internal Server Error", e);
5158
handleInternalServerError(client);
5259
} finally {
5360
if(handler != null)
@@ -68,7 +75,7 @@ private void handleInternalServerError(Socket client){
6875
out.write(response.build());
6976
out.flush();
7077
} catch (IOException e) {
71-
System.err.println("Failed to send 500 response: " + e.getMessage());
78+
log.error("Client disconnected before 500 response could be sent", e);
7279
}
7380
}
7481
}

src/main/java/org/example/filter/CompressionFilter.java

Lines changed: 94 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package org.example.filter;
22

3+
import com.aayushatharva.brotli4j.Brotli4jLoader;
34
import org.example.http.HttpResponseBuilder;
45
import org.example.httpparser.HttpRequest;
6+
import com.aayushatharva.brotli4j.encoder.Encoder;
57

68
import java.io.ByteArrayOutputStream;
79
import java.io.IOException;
@@ -10,27 +12,46 @@
1012
import 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

3240
public 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
}

src/main/java/org/example/filter/LocaleFilter.java

Lines changed: 0 additions & 98 deletions
This file was deleted.

0 commit comments

Comments
 (0)