Skip to content

Add RedirectFilter with configurable 301/302 rules#9

Closed
Ericthilen wants to merge 20 commits intomainfrom
URL-Redirect-Filter
Closed

Add RedirectFilter with configurable 301/302 rules#9
Ericthilen wants to merge 20 commits intomainfrom
URL-Redirect-Filter

Conversation

@Ericthilen
Copy link

@Ericthilen Ericthilen commented Feb 11, 2026

Implements a RedirectFilter that intercepts incoming requests and redirects them based on configurable redirect rules.

Requirements covered

  • Configurable rules: sourcePath (regex / wildcard), targetUrl, statusCode (301/302)
  • Rules evaluated in order
  • Stops the pipeline when a rule matches
  • Sets correct Location header
  • Logs each redirect

Tests

  • 301 redirect
  • 302 redirect
  • No matching rule continues pipeline
  • Wildcard matching via RedirectRulesLoader

Issue #33

Summary by CodeRabbit

  • New Features

    • Launched a configurable HTTP web server with static file serving and built-in security against path traversal attacks
    • Added support for URL redirects with configurable rules (301/302 status codes)
    • Introduced YAML/JSON configuration for server port, web root, and logging settings
    • Added Docker support for containerized deployment
    • Implemented custom error pages for 404 and 403 responses
  • Documentation

    • Comprehensive README with quick start guide, configuration examples, architecture overview, and deployment instructions
  • Tests

    • Added extensive test coverage for HTTP parsing, response building, static file handling, configuration management, and redirect functionality

Kathify and others added 9 commits February 10, 2026 14:44
Add ci workflow for github actions.
* build: configure pom.xml with needed plugin/tools.
Setup Java 25 environment with JUnit 5, Mockito, JaCoCo, Pitest, and Spotless

* fix: add missing test scope for awaitility
* release.yml that builds and publishes Docker image to GitHub packages on release.

* Fixed unverified commit stopping pull request from being merged
@coderabbitai
Copy link

coderabbitai bot commented Feb 11, 2026

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

Walkthrough

This pull request introduces a complete HTTP/TCP web server implementation in Java. The changes include a configuration-driven server core that accepts client connections, parses HTTP requests, serves static files with security protections and MIME type detection, implements a filter chain architecture for extensibility, and provides Docker containerization with automated CI/CD workflows.

Changes

Cohort / File(s) Summary
GitHub Actions Workflows
.github/workflows/ci.yml, .github/workflows/release.yml
CI workflow triggers on pushes/PRs to compile and test via Maven; release workflow publishes multi-arch Docker images to GHCR on release publish.
Project Entry Point
src/main/java/org/example/App.java
Modified to load configuration from application.yml, extract server port, and start TcpServer instead of printing hello message.
Core Server Implementation
src/main/java/org/example/TcpServer.java, src/main/java/org/example/ConnectionHandler.java
New TCP server accepting client connections on configurable port and delegating each to ConnectionHandler via virtual threads; connection handler parses HTTP requests, resolves target files, and sends responses.
HTTP Protocol Support
src/main/java/org/example/httpparser/HttpParseRequestLine.java, src/main/java/org/example/httpparser/HttpParser.java, src/main/java/org/example/httpparser/HttpRequest.java, src/main/java/org/example/http/HttpResponseBuilder.java
New HTTP request parsing (request line and headers) and response building with status line, headers, content-length, and body construction; supports both string and binary bodies.
MIME Type Detection & Static File Serving
src/main/java/org/example/http/MimeTypeDetector.java, src/main/java/org/example/StaticFileHandler.java
Utility for mapping file extensions to MIME types; static file handler serves files from configurable root with 200/404/403 responses, path traversal protection, and automatic index.html resolution.
Configuration System
src/main/java/org/example/config/AppConfig.java, src/main/java/org/example/config/ConfigLoader.java, src/main/resources/application.yml
Hierarchical config records for server port/rootDir and logging level; thread-safe cached loader supporting YAML/JSON with defaults and validation; sample config file.
Filter Architecture
src/main/java/org/example/filter/Filter.java, src/main/java/org/example/filter/FilterChain.java, src/main/java/org/example/filter/FilterChainImpl.java, src/main/java/org/example/filter/redirect/RedirectFilter.java, src/main/java/org/example/filter/redirect/RedirectRule.java, src/main/java/org/example/filter/redirect/RedirectRulesLoader.java
Extensible filter chain pattern with lifecycle methods; redirect filter implementation matching source patterns (literal, wildcard, regex) and applying 301/302 redirects.
Build & Containerization
Dockerfile, pom.xml
Removed pom.xml (project configuration deleted); added multi-stage Dockerfile building with Maven, running as non-root in Alpine JRE, exposing port 8080.
Test Suite
src/test/java/org/example/StaticFileHandlerTest.java, src/test/java/org/example/config/ConfigLoaderTest.java, src/test/java/org/example/filter/redirect/RedirectFilterTest.java, src/test/java/org/example/http/HttpResponseBuilderTest.java, src/test/java/org/example/http/MimeTypeDetectorTest.java, src/test/java/org/example/httpparser/HttpParseRequestLineTest.java, src/test/java/org/example/httpparser/HttpParserTest.java
Comprehensive unit tests covering file serving (200/404/403, path traversal), config loading (defaults, caching, validation), redirect matching, response building (status, headers, binary content, UTF-8), MIME detection, and HTTP parsing (valid/invalid requests, headers).
Documentation & Static Content
README.md, www/index.html, www/pageNotFound.html
Extensively rewrote README from hello-world to production-ready documentation with features, requirements, quick start, configuration, architecture, security, and deployment guidance; added sample HTML pages for index and 404 error.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A server springs to life, with filters and grace,
Static files served from their destined place,
Config and threading, MIME types aligned,
Docker containers to leave bugs behind,
From sockets to responses, a web of delight!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.02% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing a RedirectFilter with configurable 301/302 HTTP redirect rules, which is the primary focus of this PR.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @.github/workflows/release.yml:
- Around line 27-34: The release workflow's "Build and push" step uses
docker/build-push-action@v6.18.0 but there is no Dockerfile in the context so
the job will fail; fix by either adding a Dockerfile at the repository root (or
update the action's context and file path) that builds the application and keeps
the step using steps.meta.outputs.tags and steps.meta.outputs.labels, or
temporarily remove/guard the "Build and push" step (the step named "Build and
push") so the workflow defers push until a follow-up PR adds a Dockerfile (e.g.,
conditionally skip the step or set push: false).

In `@src/main/java/org/example/server/FilterChain.java`:
- Around line 15-18: The FilterChain constructor stores the caller's filters
array reference directly, allowing external mutation; update the
FilterChain(HttpFilter[] filters, TerminalHandler terminal) constructor to
defensively copy the incoming filters (e.g., Arrays.copyOf(filters,
filters.length) or filters.clone()) before assigning to the filters field while
still validating non-null, and keep the terminal assignment using
Objects.requireNonNull(terminal, "terminal").

In `@src/main/java/org/example/server/RedirectResponse.java`:
- Around line 3-10: Make the RedirectResponse class immutable and validate
constructor inputs: mark the fields location and statusCode as final, and in the
constructor (RedirectResponse(String location, int statusCode)) call
Objects.requireNonNull(location, "location") and validate statusCode is one of
the allowed values (e.g., 301 or 302), throwing IllegalArgumentException if not;
this prevents null locations and invalid status codes while keeping the object
effectively immutable.

In `@src/main/java/org/example/server/RedirectRulesLoader.java`:
- Around line 17-22: The regex patterns parsed in RedirectRulesLoader (when
trimmed.startsWith(REGEX_PREFIX)) are compiled raw via Pattern.compile(rawRegex)
which makes them unanchored and inconsistent with anchored wildcard/literal
rules; update the logic that handles REGEX_PREFIX to either wrap the rawRegex
with start/end anchors (prepend '^' and append '$') before compiling or provide
a clear opt-in (e.g., detect an explicit anchor flag) so regex rules behave
consistently with literal/wildcard matching; modify the block that extracts
rawRegex and calls Pattern.compile to apply the chosen anchoring behavior and
adjust the IllegalArgumentException message if needed to reflect the anchoring
policy.

In `@src/main/java/org/example/TcpServer.java`:
- Around line 19-23: The loop in TcpServer uses serverSocket.accept() and then
calls clientSocket.close() directly, which can leak the socket if an exception
occurs; update the accept/close handling to use a try-with-resources for the
Socket returned by serverSocket.accept() (e.g., wrap "Socket clientSocket =
serverSocket.accept()" in a try(...) block) so the socket is always closed, and
adjust surrounding exception handling as needed in the method containing the
loop to handle IOExceptions.
🧹 Nitpick comments (11)
src/main/java/org/example/App.java (1)

5-5: Hardcoded port and no integration with the filter pipeline.

The port is hardcoded to 8080 — consider making it configurable via CLI args or an environment variable. Also, TcpServer.start() currently just accepts and closes connections without routing them through the RedirectFilter/FilterChain pipeline introduced in this PR. Is the integration planned for a follow-up?

.github/workflows/ci.yml (2)

14-14: Inconsistent actions/checkout version across workflows.

This workflow uses actions/checkout@v4, while release.yml uses actions/checkout@v6.0.2. Consider aligning them to avoid drift and ensure consistent behavior.


16-19: mvn is invoked before the JDK is set up.

The "Get Java Version" step calls mvn help:evaluate using whatever JDK ships on the runner image, before the explicit setup-java step. This works today because ubuntu-latest bundles a default JDK, but it's fragile — a runner image update could break this. Consider either (a) setting up a known baseline JDK first, then extracting the property, then setting up the target JDK, or (b) extracting the version with a simple grep/xmllint from pom.xml instead.

Alternative: extract version without Maven
      - name: Get Java Version
        run: |
-          JAVA_VERSION=$(mvn help:evaluate "-Dexpression=maven.compiler.release" -q -DforceStdout)
+          JAVA_VERSION=$(grep -oP '(?<=<maven.compiler.release>)\d+(?=</maven.compiler.release>)' pom.xml)
           echo "JAVA_VERSION=$JAVA_VERSION" >> $GITHUB_ENV
src/main/java/org/example/TcpServer.java (2)

15-27: No shutdown mechanism — server runs until the process is killed.

start() blocks forever with no way to stop it gracefully. This complicates testing and clean shutdown. Consider adding a volatile boolean running flag and a stop() method that closes the ServerSocket to break out of accept(). The awaitility test dependency in pom.xml suggests async tests are anticipated, which would need a stoppable server.


18-18: Informational: unencrypted socket.

Static analysis flags new ServerSocket(port) as transmitting data in cleartext (CWE-319). This is acceptable for a development scaffold but should be addressed before handling real traffic (e.g., use SSLServerSocket or terminate TLS at a reverse proxy).

src/main/java/org/example/server/RedirectRulesLoader.java (1)

34-45: wildcardToRegex wraps every single character in \Q…\E — works but is noisy.

Pattern.quote(String.valueOf(c)) on each non-* character produces \Qx\E per character. This is correct but generates unnecessarily verbose regex and allocates a String per char. A simpler approach: accumulate literal runs and quote them in one call, or use Pattern.quote on segments split by *.

Simpler alternative
     private static String wildcardToRegex(String wildcard) {
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < wildcard.length(); i++) {
-            char c = wildcard.charAt(i);
-            if (c == '*') {
-                sb.append(".*");
-            } else {
-                sb.append(Pattern.quote(String.valueOf(c)));
-            }
-        }
-        return sb.toString();
+        String[] parts = wildcard.split("\\*", -1);
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < parts.length; i++) {
+            if (i > 0) sb.append(".*");
+            sb.append(Pattern.quote(parts[i]));
+        }
+        return sb.toString();
     }
pom.xml (1)

44-49: awaitility version is hardcoded — inconsistent with other dependency versions.

Other test dependencies (junit-jupiter, assertj-core, mockito) use <properties> for version management. awaitility has its version inlined. For consistency, extract it to a property.

Proposed fix

Add to <properties>:

<awaitility.version>4.3.0</awaitility.version>

Then reference it:

         <dependency>
             <groupId>org.awaitility</groupId>
             <artifactId>awaitility</artifactId>
-            <version>4.3.0</version>
+            <version>${awaitility.version}</version>
             <scope>test</scope>
         </dependency>
src/main/java/org/example/server/HttpRequest.java (1)

5-21: Consider using a Java record — you're on Java 25.

Since the project targets Java 25 and this class is already immutable with record-style accessors (method(), path()), it's a natural fit for a record. This would eliminate the boilerplate while preserving the same API.

Record equivalent
package org.example.server;

import java.util.Objects;

public record HttpRequest(String method, String path) {
    public HttpRequest {
        Objects.requireNonNull(method, "method");
        Objects.requireNonNull(path, "path");
    }
}
src/main/java/org/example/server/HttpResponse.java (1)

7-25: Consider using a case-insensitive map for HTTP headers.

HTTP header names are case-insensitive per RFC 7230. A LinkedHashMap treats "Location" and "location" as distinct keys. If other filters or future code use different casing, headers could silently duplicate. A TreeMap(String.CASE_INSENSITIVE_ORDER) is a simple drop-in fix.

♻️ Suggested change
-    private final Map<String, String> headers = new LinkedHashMap<>();
+    private final Map<String, String> headers = new java.util.TreeMap<>(String.CASE_INSENSITIVE_ORDER);
src/test/java/org/example/server/RedirectFilterTest.java (1)

33-48: Consider verifying that the chain is also short-circuited on 302 redirects.

The 301 test asserts terminalCalled is false, but the 302 test doesn't check chain termination. Adding a similar AtomicBoolean assertion here would strengthen coverage and keep the tests consistent.

src/main/java/org/example/server/FilterChain.java (1)

20-27: Terminal handler can be invoked more than once if doFilter is called after the chain is exhausted.

Once index >= filters.length, every subsequent call to doFilter re-invokes the terminal handler. If any filter or caller accidentally calls doFilter twice, the terminal runs multiple times with potentially unintended side effects. Consider adding a guard.

🛡️ Suggested guard
 public void doFilter(HttpRequest request, HttpResponse response) {
     if (index < filters.length) {
         HttpFilter current = filters[index++];
         current.doFilter(request, response, this);
         return;
     }
+    if (index++ == filters.length) {  // only invoke terminal once
-    terminal.handle(request, response);
+        terminal.handle(request, response);
+    }
 }

Comment on lines +15 to +18
public FilterChain(HttpFilter[] filters, TerminalHandler terminal) {
this.filters = Objects.requireNonNull(filters, "filters");
this.terminal = Objects.requireNonNull(terminal, "terminal");
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Defensively copy the filters array to prevent external mutation.

The constructor stores the caller's array reference directly. If the caller later modifies the array, the chain's behavior changes unexpectedly. Use Arrays.copyOf or .clone().

♻️ Suggested fix
+import java.util.Arrays;
+
 public FilterChain(HttpFilter[] filters, TerminalHandler terminal) {
-    this.filters = Objects.requireNonNull(filters, "filters");
+    this.filters = Arrays.copyOf(
+        Objects.requireNonNull(filters, "filters"), filters.length);
     this.terminal = Objects.requireNonNull(terminal, "terminal");
 }
🤖 Prompt for AI Agents
In `@src/main/java/org/example/server/FilterChain.java` around lines 15 - 18, The
FilterChain constructor stores the caller's filters array reference directly,
allowing external mutation; update the FilterChain(HttpFilter[] filters,
TerminalHandler terminal) constructor to defensively copy the incoming filters
(e.g., Arrays.copyOf(filters, filters.length) or filters.clone()) before
assigning to the filters field while still validating non-null, and keep the
terminal assignment using Objects.requireNonNull(terminal, "terminal").

Comment on lines +3 to +10
public class RedirectResponse {
private String location;
private int statusCode;

public RedirectResponse(String location, int statusCode) {
this.location = location;
this.statusCode = statusCode;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Fields should be final, and constructor should validate inputs.

location and statusCode are not final, making the object unintentionally mutable (even without setters, reflection or subclassing could mutate them). Also, unlike HttpRequest which uses Objects.requireNonNull, this class performs no validation — location can be null and statusCode can be any integer despite the PR constraining it to 301/302.

Proposed fix
 public class RedirectResponse {
-    private String location;
-    private int statusCode;
+    private final String location;
+    private final int statusCode;

     public RedirectResponse(String location, int statusCode) {
+        if (location == null || location.isBlank()) {
+            throw new IllegalArgumentException("location must not be blank");
+        }
+        if (statusCode != 301 && statusCode != 302) {
+            throw new IllegalArgumentException("statusCode must be 301 or 302");
+        }
         this.location = location;
         this.statusCode = statusCode;
     }
📝 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
public class RedirectResponse {
private String location;
private int statusCode;
public RedirectResponse(String location, int statusCode) {
this.location = location;
this.statusCode = statusCode;
}
public class RedirectResponse {
private final String location;
private final int statusCode;
public RedirectResponse(String location, int statusCode) {
if (location == null || location.isBlank()) {
throw new IllegalArgumentException("location must not be blank");
}
if (statusCode != 301 && statusCode != 302) {
throw new IllegalArgumentException("statusCode must be 301 or 302");
}
this.location = location;
this.statusCode = statusCode;
}
🤖 Prompt for AI Agents
In `@src/main/java/org/example/server/RedirectResponse.java` around lines 3 - 10,
Make the RedirectResponse class immutable and validate constructor inputs: mark
the fields location and statusCode as final, and in the constructor
(RedirectResponse(String location, int statusCode)) call
Objects.requireNonNull(location, "location") and validate statusCode is one of
the allowed values (e.g., 301 or 302), throwing IllegalArgumentException if not;
this prevents null locations and invalid status codes while keeping the object
effectively immutable.

Comment on lines +17 to +22
if (trimmed.startsWith(REGEX_PREFIX)) {
String rawRegex = trimmed.substring(REGEX_PREFIX.length());
if (rawRegex.isBlank()) {
throw new IllegalArgumentException("regex sourcePath must not be blank");
}
return Pattern.compile(rawRegex);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

regex: patterns are not anchored — inconsistent with wildcard/literal behavior.

Patterns supplied via the regex: prefix are compiled as-is (no ^/$ anchoring), while wildcard and literal patterns are always anchored. This means regex:/old will match /old-page as well as /old, but the plain literal /old will only match exactly /old. This asymmetry is likely to surprise users.

If the intent is for regex: to allow partial matches, document it. Otherwise, consider anchoring all patterns consistently, or letting the rule author opt in to partial matching.

🤖 Prompt for AI Agents
In `@src/main/java/org/example/server/RedirectRulesLoader.java` around lines 17 -
22, The regex patterns parsed in RedirectRulesLoader (when
trimmed.startsWith(REGEX_PREFIX)) are compiled raw via Pattern.compile(rawRegex)
which makes them unanchored and inconsistent with anchored wildcard/literal
rules; update the logic that handles REGEX_PREFIX to either wrap the rawRegex
with start/end anchors (prepend '^' and append '$') before compiling or provide
a clear opt-in (e.g., detect an explicit anchor flag) so regex rules behave
consistently with literal/wildcard matching; modify the block that extracts
rawRegex and calls Pattern.compile to apply the chosen anchoring behavior and
adjust the IllegalArgumentException message if needed to reflect the anchoring
policy.

Comment on lines +19 to +23
while (true) {
Socket clientSocket = serverSocket.accept(); // block
System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
clientSocket.close();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Client socket is not closed in a try-with-resources — potential leak.

If anything throws between accept() and close() (e.g., an added handler in the future), the socket leaks. Use try-with-resources for safety.

Proposed fix
             while (true) {
-                Socket clientSocket = serverSocket.accept(); // block
-                System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
-                clientSocket.close();
+                try (Socket clientSocket = serverSocket.accept()) {
+                    System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
+                }
             }
📝 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
while (true) {
Socket clientSocket = serverSocket.accept(); // block
System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
clientSocket.close();
}
while (true) {
try (Socket clientSocket = serverSocket.accept()) {
System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
}
}
🤖 Prompt for AI Agents
In `@src/main/java/org/example/TcpServer.java` around lines 19 - 23, The loop in
TcpServer uses serverSocket.accept() and then calls clientSocket.close()
directly, which can leak the socket if an exception occurs; update the
accept/close handling to use a try-with-resources for the Socket returned by
serverSocket.accept() (e.g., wrap "Socket clientSocket = serverSocket.accept()"
in a try(...) block) so the socket is always closed, and adjust surrounding
exception handling as needed in the method containing the loop to handle
IOExceptions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

🧹 Nitpick comments (28)
src/main/java/org/example/httpparser/HttpParseRequestLine.java (2)

15-15: Unnecessary variable alias.

BufferedReader reader = br; serves no purpose — just use br directly.

♻️ Proposed fix
     public void parseHttpRequest(BufferedReader br) throws IOException {
-        BufferedReader reader = br;
-        String requestLine = reader.readLine();
+        String requestLine = br.readLine();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/httpparser/HttpParseRequestLine.java` at line 15,
In HttpParseRequestLine, remove the unnecessary alias "BufferedReader reader =
br;" and update any references to "reader" to use "br" directly (or eliminate
the variable use entirely where possible); ensure no other code depends on the
"reader" symbol and delete the redundant declaration to avoid dead/duplicate
variables.

11-11: debug is hardcoded to false with no setter — the logging block is dead code.

The debug field is always false and there's no way to change it, making lines 34–38 unreachable. Either remove the dead code or expose a way to enable debug mode.

Also applies to: 34-38

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/httpparser/HttpParseRequestLine.java` at line 11,
The debug flag in class HttpParseRequestLine is hardcoded false, making the
logging block (lines 34–38) dead; fix by exposing and using a mutable debug
control: add a public setter setDebug(boolean) (or a constructor parameter) and
update any callers to enable debug when needed, then guard the existing logging
block with this.debug so the logs can be emitted; alternatively, if debug
logging is undesired, remove the debug field and the unreachable logging block
(referencing debug, setDebug, and the logging statements in
HttpParseRequestLine).
src/main/java/org/example/filter/redirect/RedirectRulesLoader.java (2)

34-45: wildcardToRegex quotes each character individually — produces verbose patterns.

Each non-* character is separately wrapped in \Q...\E (e.g., /foo\Q/\E\Qf\E\Qo\E\Qo\E). While correct, this is needlessly verbose and produces hard-to-debug regex. Batching consecutive literal characters into a single Pattern.quote() call is cleaner.

♻️ Suggested improvement
     private static String wildcardToRegex(String wildcard) {
         StringBuilder sb = new StringBuilder();
+        StringBuilder literal = new StringBuilder();
         for (int i = 0; i < wildcard.length(); i++) {
             char c = wildcard.charAt(i);
             if (c == '*') {
+                if (literal.length() > 0) {
+                    sb.append(Pattern.quote(literal.toString()));
+                    literal.setLength(0);
+                }
                 sb.append("[^/]*");
             } else {
-                sb.append(Pattern.quote(String.valueOf(c)));
+                literal.append(c);
             }
         }
+        if (literal.length() > 0) {
+            sb.append(Pattern.quote(literal.toString()));
+        }
         return sb.toString();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/redirect/RedirectRulesLoader.java` around
lines 34 - 45, The wildcardToRegex method currently calls Pattern.quote for each
non-'*' character which produces verbose regex; change it to accumulate
consecutive literal characters into a buffer and call Pattern.quote once for
each literal run, appending "[^/]*" for '*' characters as before; update the
method (wildcardToRegex) to iterate chars, build a literal chunk string, flush
it through Pattern.quote when you hit a '*' or end-of-string, and append the
quoted chunk to the StringBuilder to produce a much cleaner regex.

10-31: Anchors ^ and $ are redundant when used with Matcher.matches().

RedirectRule.matches() uses Pattern.matcher(requestPath).matches(), which inherently requires the entire string to match. The ^ and $ on Line 31 are unnecessary. Not a bug, but worth a note for clarity — either remove the anchors or document the intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/redirect/RedirectRulesLoader.java` around
lines 10 - 31, The compileSourcePattern method currently wraps the generated
regex with "^" and "$" before compiling, which is redundant because
RedirectRule.matches() uses Pattern.matcher(requestPath).matches() that already
enforces full-string matching; update compileSourcePattern (and any callers like
wildcardToRegex usage) to stop prepending "^" and "$" and simply return
Pattern.compile(regex) for both wildcard and quoted paths, or alternatively add
a comment in compileSourcePattern explaining the explicit anchors are
unnecessary but intentionally kept — reference the compileSourcePattern and
wildcardToRegex symbols and ensure unit tests for RedirectRule.matches() still
validate the intended behavior after the change.
src/main/java/org/example/filter/redirect/RedirectRule.java (1)

23-25: matches(null) will throw NPE from Pattern.matcher(null).

If requestPath is ever null, this will throw an unguarded NullPointerException. Consider whether callers can guarantee non-null, or add a defensive check.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/redirect/RedirectRule.java` around lines 23
- 25, The matches method in RedirectRule currently calls
sourcePattern.matcher(requestPath).matches() which will throw a
NullPointerException if requestPath is null; update RedirectRule.matches to
defensively handle null input (e.g., if requestPath == null return false or
throw a clear IllegalArgumentException) before calling sourcePattern.matcher,
referencing the matches method and sourcePattern.matcher to locate the change.
src/main/java/org/example/http/MimeTypeDetector.java (1)

59-65: Consider extracting "application/octet-stream" to a named constant.

The default MIME type string is used in multiple return paths. A private static final String DEFAULT_MIME_TYPE constant would reduce duplication and clarify intent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/http/MimeTypeDetector.java` around lines 59 - 65,
The default MIME string "application/octet-stream" is duplicated in
detectMimeType and other return paths; extract it into a private static final
String DEFAULT_MIME_TYPE constant (e.g., in class MimeTypeDetector) and replace
literal usages in the detectMimeType(String filename) method (and any other
methods in MimeTypeDetector that return the default) with DEFAULT_MIME_TYPE to
improve clarity and avoid duplication.
src/main/java/org/example/httpparser/HttpRequest.java (1)

11-41: Excessive indentation suggests copy-paste from a nested class.

The entire class body is indented with 8 spaces instead of the standard 4. This is cosmetic but affects readability and suggests the code was extracted from an inner class without adjusting indentation.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/httpparser/HttpRequest.java` around lines 11 - 41,
The HttpRequest class is indented with 8 spaces throughout (looks copied from a
nested class); reformat the file to use the project standard 4-space indentation
for the class body and its members (class HttpRequest, its constructor
HttpRequest(...), and all getters
getMethod/getPath/getVersion/getHeaders/getBody). Adjust the whitespace for the
field declarations, constructor parameters/body and each getter, then run the
project's Java formatter or IDE reformat action to ensure consistent indentation
and styling.
www/pageNotFound.html (1)

47-53: Consider adding a link back to the home page.

A 404 page without navigation leaves users stranded. Adding a simple link (e.g., <a href="/">Go back home</a>) improves the user experience.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@www/pageNotFound.html` around lines 47 - 53, Add a simple navigational anchor
inside the existing <div class="container"> so users can return to the
site—insert a link (e.g., an <a href="/">Go back home</a>) after the paragraph
in the 404 markup (the elements h1, h2, p inside the container) and optionally
give it a class like "home-link" for styling; update any CSS if needed to match
site styles.
Dockerfile (1)

3-6: Optimize Docker layer caching by copying pom.xml first.

Currently src/ and pom.xml are copied together before any Maven invocation. Any source change invalidates the dependency-download cache. Copying pom.xml first and resolving dependencies before copying source enables Docker to cache the dependency layer.

♻️ Proposed optimization
 FROM maven:3-eclipse-temurin-25-alpine AS build
 WORKDIR /build
-COPY src/ src/
 COPY pom.xml pom.xml
+RUN mvn dependency:go-offline
+COPY src/ src/
 RUN mvn compile
 RUN mvn dependency:copy-dependencies -DincludeScope=compile
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` around lines 3 - 6, Reorder Dockerfile steps to copy pom.xml and
download dependencies before copying source so dependency resolution layer can
be cached: first COPY pom.xml pom.xml, then RUN mvn dependency:copy-dependencies
-DincludeScope=compile (or mvn -B dependency:go-offline) to populate the
dependency layer, and only after that COPY src/ src/ and run mvn compile/build;
update the existing RUN mvn compile and RUN mvn dependency:copy-dependencies
invocations accordingly in the Dockerfile.
src/test/java/org/example/http/MimeTypeDetectorTest.java (1)

145-145: Non-English comment in an otherwise English codebase.

The Swedish comment "Parametriserad test för många filtyper på en gång" should be in English for consistency (e.g., "Parameterized test for many file types at once").

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/http/MimeTypeDetectorTest.java` at line 145, The
comment in MimeTypeDetectorTest contains a Swedish line "Parametriserad test för
många filtyper på en gång"; update this to English for consistency (e.g.,
"Parameterized test for many file types at once") in the test class
MimeTypeDetectorTest so the codebase uses a single language for comments; locate
the comment near the parameterized test method in MimeTypeDetectorTest and
replace the Swedish text with the English equivalent.
src/main/java/org/example/filter/Filter.java (1)

7-14: Consider providing default no-op implementations for init() and destroy().

Most filter implementations likely won't need lifecycle hooks. Adding default implementations reduces boilerplate for implementors.

Proposed change
 public interface Filter {
-    void init();
+    default void init() {}

     void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain);

-    void destroy();
+    default void destroy() {}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/Filter.java` around lines 7 - 14, The Filter
interface currently forces implementors to override lifecycle methods init() and
destroy(), causing unnecessary boilerplate; change Filter by adding default
no-op implementations for init() and destroy() (while keeping
doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain)
abstract) so existing implementations can omit those methods—update the
interface's init() and destroy() signatures to be default methods that do
nothing and leave doFilter unchanged.
src/main/java/org/example/filter/FilterChainImpl.java (2)

20-22: No defensive copy of the filters list.

The constructor stores the caller's list reference directly. If the caller mutates the list after constructing the chain, behavior becomes unpredictable. Wrap it with List.copyOf(filters) for safety, which also rejects null elements.

Proposed fix
     public FilterChainImpl(List<Filter> filters) {
-        this.filters = filters;
+        this.filters = List.copyOf(filters);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/FilterChainImpl.java` around lines 20 - 22,
The constructor FilterChainImpl(List<Filter> filters) stores the caller's list
reference directly which allows external mutation; change it to defensively copy
the input by assigning this.filters = List.copyOf(filters) (which also enforces
non-null and rejects null elements) so the internal filters list is immutable
and not affected by caller changes; update the FilterChainImpl constructor to
use List.copyOf and ensure the class field named filters remains compatible with
an unmodifiable/immutable list.

29-31: TODO: terminal action when all filters are exhausted.

The chain currently no-ops when all filters have run. This means the actual request handling (e.g., static file serving) never executes after filters complete. Consider accepting a terminal Runnable or handler in the constructor to invoke at the end of the chain.

Would you like me to propose an implementation for the terminal handler integration, or open an issue to track this?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/FilterChainImpl.java` around lines 29 - 31,
The chain currently no-ops when all filters are exhausted; add a terminal
handler to FilterChainImpl by introducing a final Runnable (or
Consumer<Request>/handler type) field (e.g., terminalHandler), initialize it via
the FilterChainImpl constructor, and replace the empty else block in the method
that advances the chain to call terminalHandler.run() (or invoke the handler)
when index >= filters.size(); also null-check the handler (or require non-null
in the constructor) so the terminal action always executes after all filters
finish.
src/main/java/org/example/httpparser/HttpParser.java (3)

40-45: Debug logging uses System.out.println; prefer a logging framework.

The debug flag is false by default with no setter, making this effectively dead code. If debug logging is needed, consider using java.util.logging or SLF4J, or remove this block entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/httpparser/HttpParser.java` around lines 40 - 45,
The debug printing block in HttpParser uses System.out.println and a private
debug flag that defaults to false with no setter, so this code is dead and not
using a logging framework; update the class (HttpParser) to either remove the
block or replace it with a proper logger (e.g., java.util.logging.Logger or
SLF4J) and expose control via a setter or configuration method (e.g.,
setDebug(boolean) or configureLogger), and change the prints to
logger.debug/info calls that reference headersMap and "Host" appropriately so
runtime logging is configurable and framework-based.

53-55: getHeadersMap() exposes the mutable internal map.

Callers can mutate the parser's internal state through the returned reference. Return an unmodifiable view instead.

Proposed fix
     public Map<String, String> getHeadersMap() {
-        return headersMap;
+        return Collections.unmodifiableMap(headersMap);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/httpparser/HttpParser.java` around lines 53 - 55,
getHeadersMap() in HttpParser currently returns the mutable internal headersMap,
letting callers mutate parser state; change it to return an unmodifiable view
(e.g., Collections.unmodifiableMap(headersMap) or Map.copyOf(headersMap)) from
the HttpParser#getHeadersMap method so callers cannot modify the internal map,
and keep the internal field mutable for parser operations.

16-20: setReader silently ignores subsequent calls.

The null guard on line 17 means a second call with a different InputStream is silently discarded. This can mask bugs if an HttpParser instance is inadvertently reused with a new connection. Consider either throwing an IllegalStateException on re-initialization, or allowing re-assignment (resetting the headers map as well).

Option: throw on re-init
     public void setReader(InputStream in) {
-        if (this.reader == null) {
-            this.reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
+        if (this.reader != null) {
+            throw new IllegalStateException("Reader already initialized");
         }
+        this.reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/httpparser/HttpParser.java` around lines 16 - 20,
The setReader method in HttpParser currently ignores subsequent calls because of
the null guard on reader; change it to fail fast by throwing an
IllegalStateException when reader is already set (e.g., if (this.reader != null)
throw new IllegalStateException("HttpParser already initialized with a
reader")); otherwise, if you prefer re-use semantics, allow re-assignment by
replacing the reader assignment with unconditional reinitialization and also
reset internal parsing state (e.g., clear the headers map field via
headers.clear() and any other state) before setting this.reader to the new
BufferedReader; update the setReader method accordingly.
src/test/java/org/example/httpparser/HttpParserTest.java (2)

16-16: Test method name TestHttpParserForHeaders starts with an uppercase letter.

Java convention uses camelCase for method names: testHttpParserForHeaders.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/httpparser/HttpParserTest.java` at line 16, Method
name TestHttpParserForHeaders violates Java naming conventions; rename the test
method declaration from TestHttpParserForHeaders to testHttpParserForHeaders and
update any references/usages (e.g., in annotations or reflective lookups) to
match the new camelCase name so the test framework still discovers and runs it.

9-10: Mixing AssertJ and JUnit assertion APIs.

The file imports assertThat from AssertJ (line 9) and assertNotNull/assertEquals from JUnit (line 10). Pick one assertion library for consistency—preferably AssertJ throughout, since it's already used for most assertions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/httpparser/HttpParserTest.java` around lines 9 -
10, The test mixes AssertJ and JUnit assertions; in HttpParserTest remove the
static import of JUnit assertions and convert all usages of JUnit methods (e.g.,
assertNotNull, assertEquals, assertTrue, assertFalse) to AssertJ equivalents
using assertThat (e.g., assertThat(obj).isNotNull(),
assertThat(actual).isEqualTo(expected), assertThat(cond).isTrue(),
assertThat(cond).isFalse()). Ensure the file keeps the existing static import
import static org.assertj.core.api.AssertionsForClassTypes.assertThat; and
update every assertion site in HttpParserTest to use assertThat so imports and
assertions are consistent.
pom.xml (1)

113-115: Surefire argLine relies on maven-dependency-plugin:properties goal for Mockito agent path.

The expression ${org.mockito:mockito-core:jar} is resolved by the properties goal of maven-dependency-plugin (lines 97–107). This is a correct but fragile setup—if the dependency-plugin execution is removed or the Mockito dependency changes, the build will fail with an unresolved property. Consider adding a brief XML comment explaining this coupling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pom.xml` around lines 113 - 115, The surefire argLine uses the property
${org.mockito:mockito-core:jar} which is produced by the maven-dependency-plugin
execution (properties goal) and is fragile if that execution or the Mockito
dependency changes; add a short XML comment near the <argLine> element
explaining this coupling and warning that the property is provided by the
maven-dependency-plugin:properties execution and must be kept in sync with the
Mockito dependency (or replaced with a stable property), and optionally mention
the plugin goal id (maven-dependency-plugin:properties) and the dependency
coordinate (org.mockito:mockito-core) so future maintainers know where the value
originates and how to fix it if unresolved.
src/test/java/org/example/httpparser/HttpParseRequestLineTest.java (2)

8-10: Same assertion framework mixing as HttpParserTest.

Both AssertJ (assertThat, assertThatThrownBy) and JUnit (assertThrows) are used. For consistency, consider using one. assertThatThrownBy can fully replace assertThrows + assertThat on the message.

Example: consolidate lines 39-48 with AssertJ only
     `@Test`
     void testParserThrowErrorWhenEmpty(){
         InputStream in = new ByteArrayInputStream("".getBytes());
         httpParseRequestLine.setReader(in);
-        Exception exception = assertThrows(
-                IOException.class, () -> httpParseRequestLine.parseRequest()
-        );
-
-        assertThat(exception.getMessage()).isEqualTo("HTTP Request Line is Null or Empty");
+        assertThatThrownBy(() -> httpParseRequestLine.parseRequest())
+                .isInstanceOf(IOException.class)
+                .hasMessage("HTTP Request Line is Null or Empty");
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/httpparser/HttpParseRequestLineTest.java` around
lines 8 - 10, The test imports mix AssertJ and JUnit assertions; consolidate to
AssertJ only by removing JUnit assertions import and replacing uses of JUnit's
assertThrows + subsequent assertThat on the exception message with a single
assertThatThrownBy call; update tests in HttpParseRequestLineTest (and any
methods that call assertThrows) to use
assertThatThrownBy(...).isInstanceOf(...).hasMessageContaining(...) or
equivalent AssertJ assertions so only assertThat/assertThatThrownBy from AssertJ
are used.

20-31: getBytes() without explicit charset.

Lines 24, 41, 53, 64 all use testString.getBytes() (platform-default charset) instead of getBytes(StandardCharsets.UTF_8). Since the production code reads with StandardCharsets.UTF_8, tests should encode consistently to avoid platform-dependent failures.

Example fix (apply similarly at lines 41, 53, 64)
-        InputStream in = new ByteArrayInputStream(testString.getBytes());
+        InputStream in = new ByteArrayInputStream(testString.getBytes(StandardCharsets.UTF_8));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/httpparser/HttpParseRequestLineTest.java` around
lines 20 - 31, Tests in HttpParseRequestLineTest call testString.getBytes()
(e.g., inside testParserWithTestRequestLine) which uses the platform default
charset and can cause flaky behavior; update each test to encode the request
byte streams with an explicit charset by replacing getBytes() with
getBytes(StandardCharsets.UTF_8) in testParserWithTestRequestLine and the other
test methods referenced (lines ~41, ~53, ~64) so the InputStream fed to
httpParseRequestLine.setReader(...) matches the production UTF-8 decoding.
src/main/java/org/example/http/HttpResponseBuilder.java (2)

45-48: setBody(byte[]) stores the array reference without a defensive copy.

If the caller mutates the array after calling setBody, the built response will contain the mutated data. This is a minor concern for an internal builder but worth noting if the API surface grows.

Proposed fix
     public void setBody(byte[] body) {
-        this.bytebody = body;
+        this.bytebody = body != null ? body.clone() : null;
         this.body = "";
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/http/HttpResponseBuilder.java` around lines 45 -
48, setBody(byte[]) currently stores the caller's byte array reference in the
bytebody field which allows external mutation; change setBody(byte[] body) to
make a defensive copy (e.g., Arrays.copyOf(body, body.length) or body.clone())
before assigning to the bytebody field and keep resetting the string body to "";
also handle null input by setting bytebody to null. Ensure you update only the
setBody method and reference the bytebody field and body field in your change.

64-67: Javadoc comment uses /* instead of /** — won't be picked up by documentation tools.

Proposed fix
-    /*
+    /**
      * Builds the complete HTTP response as a byte array and preserves binary content without corruption.
      * `@return` Complete HTTP response (headers + body) as byte[]
      */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/http/HttpResponseBuilder.java` around lines 64 -
67, The block comment above the response-building routine in HttpResponseBuilder
is a regular C-style comment (/*) so Javadoc tools will ignore it; change it to
a Javadoc comment (/** ... */) and ensure the `@return` tag is properly formatted
inside that Javadoc so the method (e.g., the response-building method in class
HttpResponseBuilder such as buildResponse or build) is documented correctly and
the description notes that it returns the complete HTTP response as a byte[]
preserving binary content.
src/main/java/org/example/TcpServer.java (1)

20-22: Virtual thread exception goes unobserved.

If handleClient throws, the RuntimeException propagates inside the virtual thread with no UncaughtExceptionHandler. The default handler prints to stderr, but the server has no visibility into per-connection failures. Consider catching and logging inside the lambda, or setting a thread-level uncaught exception handler.

Proposed fix
-                Thread.ofVirtual().start(() -> handleClient(clientSocket));
+                Thread.ofVirtual().start(() -> {
+                    try {
+                        handleClient(clientSocket);
+                    } catch (Exception e) {
+                        System.err.println("Client handler failed: " + e.getMessage());
+                    }
+                });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/TcpServer.java` around lines 20 - 22, The
virtual-thread lambda that starts handleClient via Thread.ofVirtual().start(()
-> handleClient(clientSocket)) does not observe exceptions; wrap the call to
handleClient(clientSocket) in a try/catch inside the lambda (or install a
Thread.setDefaultUncaughtExceptionHandler/Thread.UncaughtExceptionHandler for
these virtual threads) and log the exception with your server logger including
clientSocket.getRemoteSocketAddress(), ensure the clientSocket is closed in a
finally block to free resources, and propagate/record any relevant metrics or
status for per-connection failures.
src/main/java/org/example/ConnectionHandler.java (1)

10-11: Fields should be private.

client and uri have package-private access but are only used within this class. Mark them private to enforce encapsulation.

Proposed fix
-    Socket client;
-    String uri;
+    private final Socket client;
+    private String uri;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/ConnectionHandler.java` around lines 10 - 11,
Change the access modifiers of the fields in ConnectionHandler: make the
package-private Socket client and String uri private (i.e., private Socket
client; private String uri;). Ensure any external access is replaced with
getters/setters if required (but if they are only used inside ConnectionHandler
no further changes are needed) and rebuild to verify no visibility errors.
src/main/java/org/example/StaticFileHandler.java (1)

10-13: Mutable instance fields (fileBytes, statusCode) for inter-method state are fragile.

handleGetRequest communicates results to sendGetRequest via instance fields rather than a return value. This makes the class not thread-safe and tightly couples the two methods. Consider returning a small result record (e.g., record FileResult(byte[] content, int status)) from handleGetRequest.

Also, WEB_ROOT is an instance field but named like a static final constant — prefer webRoot per Java naming conventions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/StaticFileHandler.java` around lines 10 - 13, The
class uses mutable instance fields fileBytes and statusCode to pass data between
handleGetRequest and sendGetRequest and exposes an instance field WEB_ROOT named
like a constant; refactor by introducing an immutable result type (e.g., record
FileResult(byte[] content, int status)) returned from handleGetRequest, update
sendGetRequest to accept a FileResult (or its content and status) instead of
reading instance fields, remove the fileBytes and statusCode fields, and rename
WEB_ROOT to webRoot (and keep it final if intended immutable) updating all
usages in StaticFileHandler.
src/test/java/org/example/config/ConfigLoaderTest.java (1)

128-140: Inconsistent method naming and extra whitespace.

invalid_port_should_Throw_Exception mixes snake_case with PascalCase, unlike every other test in this file. There's also extraneous whitespace before ().

-    void invalid_port_should_Throw_Exception    () throws Exception {
+    void invalid_port_should_throw_exception() throws Exception {

Also, the throws Exception declaration is unnecessary here since no checked exception escapes — assertThatThrownBy catches it internally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/config/ConfigLoaderTest.java` around lines 128 -
140, Rename and clean up the test method invalid_port_should_Throw_Exception:
change its name to follow the file's naming convention (e.g.,
invalidPortShouldThrowException or invalidPortThrowsException), remove the
extraneous whitespace before the parentheses, and delete the unnecessary "throws
Exception" declaration since assertThatThrownBy handles exceptions; leave the
assertion using ConfigLoader.loadOnce(configFile) and assertThatThrownBy
unchanged.
src/main/java/org/example/config/AppConfig.java (1)

26-37: Default values are duplicated between defaults() and withDefaultsApplied().

The port 8080, root dir "./www", and logging level "INFO" are hardcoded in both defaults() and withDefaultsApplied(). If one is updated without the other, they'll silently diverge. Consider extracting constants:

♻️ Suggested refactor
+    private static final int DEFAULT_PORT = 8080;
+    private static final String DEFAULT_ROOT_DIR = "./www";
+
     public record ServerConfig(
             `@JsonProperty`("port") Integer port,
             `@JsonProperty`("rootDir") String rootDir
     ) {
         public static ServerConfig defaults() {
-            return new ServerConfig(8080, "./www");
+            return new ServerConfig(DEFAULT_PORT, DEFAULT_ROOT_DIR);
         }
 
         public ServerConfig withDefaultsApplied() {
-            int p = (port == null ? 8080 : port);
+            int p = (port == null ? DEFAULT_PORT : port);
             if (p < 1 || p > 65535) {
                 throw new IllegalArgumentException("Invalid port number: " + p + ". Port must be between 1 and 65535");
             }
-            String rd = (rootDir == null || rootDir.isBlank()) ? "./www" : rootDir;
+            String rd = (rootDir == null || rootDir.isBlank()) ? DEFAULT_ROOT_DIR : rootDir;
             return new ServerConfig(p, rd);
         }
     }

Same applies to LoggingConfig with "INFO".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/config/AppConfig.java` around lines 26 - 37, The
defaults (port 8080, root dir "./www", and log level "INFO") are duplicated
between ServerConfig.defaults() and withDefaultsApplied(), and similarly in
LoggingConfig; extract shared constants (e.g. DEFAULT_PORT, DEFAULT_ROOT_DIR,
DEFAULT_LOG_LEVEL) at the class level or a central AppConfig constants holder
and use those constants inside ServerConfig.defaults(),
ServerConfig.withDefaultsApplied(), and LoggingConfig to set and validate
defaults (replace hardcoded 8080, "./www", and "INFO" with the new constants and
reuse them for validation and return values).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Dockerfile`:
- Line 12: The Dockerfile currently copies compiled classes to root using the
COPY instruction "COPY --from=build /build/target/classes/ /", but the Java
classpath in the ENTRYPOINT expects classes under /app (classpath
"/app:/dependencies/*"), causing ClassNotFoundException; update the COPY to
place the build output into /app (so files from /build/target/classes/ arrive
under /app) so the runtime classpath matches the actual file location.

In `@README.md`:
- Around line 329-336: Update the README section that currently labels "Filter
Chain (Future Feature)" to document the new RedirectFilter, RedirectRule, and
RedirectRulesLoader: replace the "future feature" wording with a brief
description of RedirectFilter behavior, show how RedirectRule entries are
structured and loaded via RedirectRulesLoader, and mention the filter chain
remains extensible for adding more filters (e.g., authentication, logging,
compression); reference the RedirectFilter, RedirectRule, and
RedirectRulesLoader names so readers can find the implementations.
- Line 48: The README's repository clone command contains a duplicated keyword
("git clone git clone ..."); update the command text to a single correct
invocation (e.g., "git clone
https://github.com/ithsjava25/project-webserver-juv25g.git") so the README shows
a valid clone command.
- Around line 37-44: Remove the extra space in the level-2 heading so "##  Quick
Start" becomes "## Quick Start", and add appropriate fenced-code block language
identifiers (e.g., text, bash) to each triple-backtick block present in
README.md—specifically the ASCII-art/server launch block under the Quick Start
heading and the other fenced blocks further down (the examples and command
snippets) so they render with proper syntax highlighting and satisfy markdown
lint rules; update only the heading text and the opening backticks for the
fenced blocks (no content changes).
- Line 12: The link text "[Features](`#features`) • [Quick Start](`#quick-start`) •
[Configuration](`#configuration`)" uses the wrong anchor for the "## ✨ Features"
heading; update the Features link to use the generated slug by changing
"(`#features`)" to "(`#-features`)" so the anchor matches the "## ✨ Features"
heading and the link navigates correctly.

In `@src/main/java/org/example/App.java`:
- Line 10: The code currently constructs Path configPath =
Path.of("src/main/resources/application.yml") in App, which fails in packaged
JARs; change App to accept an optional CLI argument for the config file path
(e.g., check args[] for a provided path and use that to build configPath) and if
no arg is provided, load the resource from the classpath instead (use
ClassLoader#getResourceAsStream) and pass that to ConfigLoader as a fallback;
ensure ConfigLoader is used unchanged where it already handles missing
files/defaults so the new logic tries the explicit file first, then the
classpath resource.

In `@src/main/java/org/example/config/ConfigLoader.java`:
- Around line 55-66: The createMapperFor method uses the old Jackson 2.x pattern
YAMLMapper.builder(new YAMLFactory()) which is incompatible with Jackson 3.x;
update both YAMLMapper.builder(new YAMLFactory()) calls in createMapperFor to
YAMLMapper.builder().build() (i.e., call builder() with no args and then
build()), remove the now-unused YAMLFactory import, and ensure the
JsonMapper.builder().build() branch remains unchanged.

In `@src/main/java/org/example/ConnectionHandler.java`:
- Around line 27-35: The resolveTargetFile method in ConnectionHandler is
appending ".html" to any URI that doesn't end with ".html", which also mangles
assets like "/style.css" or "/image.png"; change the logic in resolveTargetFile
so it only appends ".html" for extensionless paths (e.g., check for absence of a
file extension by comparing lastIndexOf('.') to lastIndexOf('/') or by using a
regex that ensures there is no dot after the last slash) and leave URIs that
already have any extension unchanged; update references to this.uri assignment
accordingly so index root ("/") still maps to "index.html" and only true
extensionless paths get the ".html" suffix.
- Around line 17-25: runConnectionHandler currently parses the request and
directly calls StaticFileHandler.sendGetRequest, skipping the RedirectFilter and
FilterChain; modify runConnectionHandler to build and invoke the filter pipeline
after parsing (use HttpParser/parser to obtain request data and
resolveTargetFile to set uri), instantiate a FilterChain, add RedirectFilter
(and any other filters), then call the chain to process the request and let the
chain decide whether to forward to StaticFileHandler.sendGetRequest or perform a
redirect; ensure the code uses the same request/response objects (from
client.getInputStream()/getOutputStream()) and that resolveTargetFile/uri are
updated only after filters run if filters can mutate the target.

In `@src/main/java/org/example/httpparser/HttpRequest.java`:
- Around line 19-29: The HttpRequest constructor allows a null path which leads
to NPE when RedirectFilter/RedirectRule.matches() calls
Pattern.matcher(requestPath). Add null checks by calling
Objects.requireNonNull(...) on path, method, and version at the start of the
HttpRequest constructor (and import java.util.Objects if missing) so the
constructor enforces non-null contracts and fails fast with a clear message.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 58-66: sendGetRequest currently passes the raw uri to
setContentTypeFromFilename, causing MimeTypeDetector.detectMimeType to see query
strings/fragments (e.g., ".html?foo=bar") and return wrong MIME;
sanitize/normalize the URI before MIME detection by reusing the sanitized value
produced in handleGetRequest (or have handleGetRequest return the sanitized path
or store it on the instance), then call setContentTypeFromFilename with that
sanitized filename/path (ensuring query string and fragment are removed) so
Content-Type is derived from the cleaned filename rather than the original uri.

In `@src/main/java/org/example/TcpServer.java`:
- Around line 32-34: The catch block in TcpServer currently throws a
RuntimeException by concatenating the caught exception into the message, which
discards the original stack trace; change the throw to use the two-argument
constructor so the caught exception becomes the cause (e.g., replace the
existing throw in the client-handling catch with new RuntimeException("Error
handling client connection", e)) to preserve the original exception and stack
trace.

In `@src/test/java/org/example/http/HttpResponseBuilderTest.java`:
- Around line 269-284: The test build_handlesUnknownStatusCodes in
HttpResponseBuilderTest mistakenly treats 100 as an "unknown" status; update the
ValueSource in that parameterized test so it uses only non-standard codes (e.g.,
replace 100 with 299 or 499) to reflect the intent, keeping the test method name
and assertions the same; locate the ValueSource ints array in
build_handlesUnknownStatusCodes and swap the 100 entry for a truly non-standard
code.

In `@src/test/java/org/example/httpparser/HttpParserTest.java`:
- Around line 15-28: The test TestHttpParserForHeaders feeds a full HTTP request
but calls parseHttp() directly, so the request line ("GET /index.html...") is
not consumed properly; update the test to first call httpParser.parseRequest()
after setting the input stream (httpParser.setReader(...)) and then call
httpParser.parseHttp() so the request line is consumed by parseRequest() before
header parsing begins (adjust TestHttpParserForHeaders to invoke parseRequest()
then parseHttp()).

---

Nitpick comments:
In `@Dockerfile`:
- Around line 3-6: Reorder Dockerfile steps to copy pom.xml and download
dependencies before copying source so dependency resolution layer can be cached:
first COPY pom.xml pom.xml, then RUN mvn dependency:copy-dependencies
-DincludeScope=compile (or mvn -B dependency:go-offline) to populate the
dependency layer, and only after that COPY src/ src/ and run mvn compile/build;
update the existing RUN mvn compile and RUN mvn dependency:copy-dependencies
invocations accordingly in the Dockerfile.

In `@pom.xml`:
- Around line 113-115: The surefire argLine uses the property
${org.mockito:mockito-core:jar} which is produced by the maven-dependency-plugin
execution (properties goal) and is fragile if that execution or the Mockito
dependency changes; add a short XML comment near the <argLine> element
explaining this coupling and warning that the property is provided by the
maven-dependency-plugin:properties execution and must be kept in sync with the
Mockito dependency (or replaced with a stable property), and optionally mention
the plugin goal id (maven-dependency-plugin:properties) and the dependency
coordinate (org.mockito:mockito-core) so future maintainers know where the value
originates and how to fix it if unresolved.

In `@src/main/java/org/example/config/AppConfig.java`:
- Around line 26-37: The defaults (port 8080, root dir "./www", and log level
"INFO") are duplicated between ServerConfig.defaults() and
withDefaultsApplied(), and similarly in LoggingConfig; extract shared constants
(e.g. DEFAULT_PORT, DEFAULT_ROOT_DIR, DEFAULT_LOG_LEVEL) at the class level or a
central AppConfig constants holder and use those constants inside
ServerConfig.defaults(), ServerConfig.withDefaultsApplied(), and LoggingConfig
to set and validate defaults (replace hardcoded 8080, "./www", and "INFO" with
the new constants and reuse them for validation and return values).

In `@src/main/java/org/example/ConnectionHandler.java`:
- Around line 10-11: Change the access modifiers of the fields in
ConnectionHandler: make the package-private Socket client and String uri private
(i.e., private Socket client; private String uri;). Ensure any external access
is replaced with getters/setters if required (but if they are only used inside
ConnectionHandler no further changes are needed) and rebuild to verify no
visibility errors.

In `@src/main/java/org/example/filter/Filter.java`:
- Around line 7-14: The Filter interface currently forces implementors to
override lifecycle methods init() and destroy(), causing unnecessary
boilerplate; change Filter by adding default no-op implementations for init()
and destroy() (while keeping doFilter(HttpRequest request, HttpResponseBuilder
response, FilterChain chain) abstract) so existing implementations can omit
those methods—update the interface's init() and destroy() signatures to be
default methods that do nothing and leave doFilter unchanged.

In `@src/main/java/org/example/filter/FilterChainImpl.java`:
- Around line 20-22: The constructor FilterChainImpl(List<Filter> filters)
stores the caller's list reference directly which allows external mutation;
change it to defensively copy the input by assigning this.filters =
List.copyOf(filters) (which also enforces non-null and rejects null elements) so
the internal filters list is immutable and not affected by caller changes;
update the FilterChainImpl constructor to use List.copyOf and ensure the class
field named filters remains compatible with an unmodifiable/immutable list.
- Around line 29-31: The chain currently no-ops when all filters are exhausted;
add a terminal handler to FilterChainImpl by introducing a final Runnable (or
Consumer<Request>/handler type) field (e.g., terminalHandler), initialize it via
the FilterChainImpl constructor, and replace the empty else block in the method
that advances the chain to call terminalHandler.run() (or invoke the handler)
when index >= filters.size(); also null-check the handler (or require non-null
in the constructor) so the terminal action always executes after all filters
finish.

In `@src/main/java/org/example/filter/redirect/RedirectRule.java`:
- Around line 23-25: The matches method in RedirectRule currently calls
sourcePattern.matcher(requestPath).matches() which will throw a
NullPointerException if requestPath is null; update RedirectRule.matches to
defensively handle null input (e.g., if requestPath == null return false or
throw a clear IllegalArgumentException) before calling sourcePattern.matcher,
referencing the matches method and sourcePattern.matcher to locate the change.

In `@src/main/java/org/example/filter/redirect/RedirectRulesLoader.java`:
- Around line 34-45: The wildcardToRegex method currently calls Pattern.quote
for each non-'*' character which produces verbose regex; change it to accumulate
consecutive literal characters into a buffer and call Pattern.quote once for
each literal run, appending "[^/]*" for '*' characters as before; update the
method (wildcardToRegex) to iterate chars, build a literal chunk string, flush
it through Pattern.quote when you hit a '*' or end-of-string, and append the
quoted chunk to the StringBuilder to produce a much cleaner regex.
- Around line 10-31: The compileSourcePattern method currently wraps the
generated regex with "^" and "$" before compiling, which is redundant because
RedirectRule.matches() uses Pattern.matcher(requestPath).matches() that already
enforces full-string matching; update compileSourcePattern (and any callers like
wildcardToRegex usage) to stop prepending "^" and "$" and simply return
Pattern.compile(regex) for both wildcard and quoted paths, or alternatively add
a comment in compileSourcePattern explaining the explicit anchors are
unnecessary but intentionally kept — reference the compileSourcePattern and
wildcardToRegex symbols and ensure unit tests for RedirectRule.matches() still
validate the intended behavior after the change.

In `@src/main/java/org/example/http/HttpResponseBuilder.java`:
- Around line 45-48: setBody(byte[]) currently stores the caller's byte array
reference in the bytebody field which allows external mutation; change
setBody(byte[] body) to make a defensive copy (e.g., Arrays.copyOf(body,
body.length) or body.clone()) before assigning to the bytebody field and keep
resetting the string body to ""; also handle null input by setting bytebody to
null. Ensure you update only the setBody method and reference the bytebody field
and body field in your change.
- Around line 64-67: The block comment above the response-building routine in
HttpResponseBuilder is a regular C-style comment (/*) so Javadoc tools will
ignore it; change it to a Javadoc comment (/** ... */) and ensure the `@return`
tag is properly formatted inside that Javadoc so the method (e.g., the
response-building method in class HttpResponseBuilder such as buildResponse or
build) is documented correctly and the description notes that it returns the
complete HTTP response as a byte[] preserving binary content.

In `@src/main/java/org/example/http/MimeTypeDetector.java`:
- Around line 59-65: The default MIME string "application/octet-stream" is
duplicated in detectMimeType and other return paths; extract it into a private
static final String DEFAULT_MIME_TYPE constant (e.g., in class MimeTypeDetector)
and replace literal usages in the detectMimeType(String filename) method (and
any other methods in MimeTypeDetector that return the default) with
DEFAULT_MIME_TYPE to improve clarity and avoid duplication.

In `@src/main/java/org/example/httpparser/HttpParser.java`:
- Around line 40-45: The debug printing block in HttpParser uses
System.out.println and a private debug flag that defaults to false with no
setter, so this code is dead and not using a logging framework; update the class
(HttpParser) to either remove the block or replace it with a proper logger
(e.g., java.util.logging.Logger or SLF4J) and expose control via a setter or
configuration method (e.g., setDebug(boolean) or configureLogger), and change
the prints to logger.debug/info calls that reference headersMap and "Host"
appropriately so runtime logging is configurable and framework-based.
- Around line 53-55: getHeadersMap() in HttpParser currently returns the mutable
internal headersMap, letting callers mutate parser state; change it to return an
unmodifiable view (e.g., Collections.unmodifiableMap(headersMap) or
Map.copyOf(headersMap)) from the HttpParser#getHeadersMap method so callers
cannot modify the internal map, and keep the internal field mutable for parser
operations.
- Around line 16-20: The setReader method in HttpParser currently ignores
subsequent calls because of the null guard on reader; change it to fail fast by
throwing an IllegalStateException when reader is already set (e.g., if
(this.reader != null) throw new IllegalStateException("HttpParser already
initialized with a reader")); otherwise, if you prefer re-use semantics, allow
re-assignment by replacing the reader assignment with unconditional
reinitialization and also reset internal parsing state (e.g., clear the headers
map field via headers.clear() and any other state) before setting this.reader to
the new BufferedReader; update the setReader method accordingly.

In `@src/main/java/org/example/httpparser/HttpParseRequestLine.java`:
- Line 15: In HttpParseRequestLine, remove the unnecessary alias "BufferedReader
reader = br;" and update any references to "reader" to use "br" directly (or
eliminate the variable use entirely where possible); ensure no other code
depends on the "reader" symbol and delete the redundant declaration to avoid
dead/duplicate variables.
- Line 11: The debug flag in class HttpParseRequestLine is hardcoded false,
making the logging block (lines 34–38) dead; fix by exposing and using a mutable
debug control: add a public setter setDebug(boolean) (or a constructor
parameter) and update any callers to enable debug when needed, then guard the
existing logging block with this.debug so the logs can be emitted;
alternatively, if debug logging is undesired, remove the debug field and the
unreachable logging block (referencing debug, setDebug, and the logging
statements in HttpParseRequestLine).

In `@src/main/java/org/example/httpparser/HttpRequest.java`:
- Around line 11-41: The HttpRequest class is indented with 8 spaces throughout
(looks copied from a nested class); reformat the file to use the project
standard 4-space indentation for the class body and its members (class
HttpRequest, its constructor HttpRequest(...), and all getters
getMethod/getPath/getVersion/getHeaders/getBody). Adjust the whitespace for the
field declarations, constructor parameters/body and each getter, then run the
project's Java formatter or IDE reformat action to ensure consistent indentation
and styling.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 10-13: The class uses mutable instance fields fileBytes and
statusCode to pass data between handleGetRequest and sendGetRequest and exposes
an instance field WEB_ROOT named like a constant; refactor by introducing an
immutable result type (e.g., record FileResult(byte[] content, int status))
returned from handleGetRequest, update sendGetRequest to accept a FileResult (or
its content and status) instead of reading instance fields, remove the fileBytes
and statusCode fields, and rename WEB_ROOT to webRoot (and keep it final if
intended immutable) updating all usages in StaticFileHandler.

In `@src/main/java/org/example/TcpServer.java`:
- Around line 20-22: The virtual-thread lambda that starts handleClient via
Thread.ofVirtual().start(() -> handleClient(clientSocket)) does not observe
exceptions; wrap the call to handleClient(clientSocket) in a try/catch inside
the lambda (or install a
Thread.setDefaultUncaughtExceptionHandler/Thread.UncaughtExceptionHandler for
these virtual threads) and log the exception with your server logger including
clientSocket.getRemoteSocketAddress(), ensure the clientSocket is closed in a
finally block to free resources, and propagate/record any relevant metrics or
status for per-connection failures.

In `@src/test/java/org/example/config/ConfigLoaderTest.java`:
- Around line 128-140: Rename and clean up the test method
invalid_port_should_Throw_Exception: change its name to follow the file's naming
convention (e.g., invalidPortShouldThrowException or
invalidPortThrowsException), remove the extraneous whitespace before the
parentheses, and delete the unnecessary "throws Exception" declaration since
assertThatThrownBy handles exceptions; leave the assertion using
ConfigLoader.loadOnce(configFile) and assertThatThrownBy unchanged.

In `@src/test/java/org/example/http/MimeTypeDetectorTest.java`:
- Line 145: The comment in MimeTypeDetectorTest contains a Swedish line
"Parametriserad test för många filtyper på en gång"; update this to English for
consistency (e.g., "Parameterized test for many file types at once") in the test
class MimeTypeDetectorTest so the codebase uses a single language for comments;
locate the comment near the parameterized test method in MimeTypeDetectorTest
and replace the Swedish text with the English equivalent.

In `@src/test/java/org/example/httpparser/HttpParseRequestLineTest.java`:
- Around line 8-10: The test imports mix AssertJ and JUnit assertions;
consolidate to AssertJ only by removing JUnit assertions import and replacing
uses of JUnit's assertThrows + subsequent assertThat on the exception message
with a single assertThatThrownBy call; update tests in HttpParseRequestLineTest
(and any methods that call assertThrows) to use
assertThatThrownBy(...).isInstanceOf(...).hasMessageContaining(...) or
equivalent AssertJ assertions so only assertThat/assertThatThrownBy from AssertJ
are used.
- Around line 20-31: Tests in HttpParseRequestLineTest call
testString.getBytes() (e.g., inside testParserWithTestRequestLine) which uses
the platform default charset and can cause flaky behavior; update each test to
encode the request byte streams with an explicit charset by replacing getBytes()
with getBytes(StandardCharsets.UTF_8) in testParserWithTestRequestLine and the
other test methods referenced (lines ~41, ~53, ~64) so the InputStream fed to
httpParseRequestLine.setReader(...) matches the production UTF-8 decoding.

In `@src/test/java/org/example/httpparser/HttpParserTest.java`:
- Line 16: Method name TestHttpParserForHeaders violates Java naming
conventions; rename the test method declaration from TestHttpParserForHeaders to
testHttpParserForHeaders and update any references/usages (e.g., in annotations
or reflective lookups) to match the new camelCase name so the test framework
still discovers and runs it.
- Around line 9-10: The test mixes AssertJ and JUnit assertions; in
HttpParserTest remove the static import of JUnit assertions and convert all
usages of JUnit methods (e.g., assertNotNull, assertEquals, assertTrue,
assertFalse) to AssertJ equivalents using assertThat (e.g.,
assertThat(obj).isNotNull(), assertThat(actual).isEqualTo(expected),
assertThat(cond).isTrue(), assertThat(cond).isFalse()). Ensure the file keeps
the existing static import import static
org.assertj.core.api.AssertionsForClassTypes.assertThat; and update every
assertion site in HttpParserTest to use assertThat so imports and assertions are
consistent.

In `@www/pageNotFound.html`:
- Around line 47-53: Add a simple navigational anchor inside the existing <div
class="container"> so users can return to the site—insert a link (e.g., an <a
href="/">Go back home</a>) after the paragraph in the 404 markup (the elements
h1, h2, p inside the container) and optionally give it a class like "home-link"
for styling; update any CSS if needed to match site styles.

Dockerfile Outdated
EXPOSE 8080
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app/
COPY --from=build /build/target/classes/ /
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Classes copied to / but classpath expects /app — the app won't start.

COPY --from=build /build/target/classes/ / places compiled classes at the filesystem root (e.g., /org/example/App.class). The ENTRYPOINT classpath is /app:/dependencies/*, so Java will look for org.example.App under /app/org/example/App.class and fail with ClassNotFoundException.

🐛 Proposed fix
-COPY --from=build /build/target/classes/ /
+COPY --from=build /build/target/classes/ /app/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 12, The Dockerfile currently copies compiled classes to
root using the COPY instruction "COPY --from=build /build/target/classes/ /",
but the Java classpath in the ENTRYPOINT expects classes under /app (classpath
"/app:/dependencies/*"), causing ClassNotFoundException; update the COPY to
place the build output into /app (so files from /build/target/classes/ arrive
under /app) so the runtime classpath matches the actual file location.

README.md Outdated

*A modern, high-performance HTTP web server built from scratch in Java*

[Features](#features) • [Quick Start](#quick-start) • [Configuration](#configuration)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat README.md

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 9195


🌐 Web query:

GitHub anchor generation emoji heading rules

💡 Result:

GitHub’s auto-generated heading anchors treat emoji as punctuation, so emoji characters themselves don’t become part of the slug; they only influence whether a hyphen is inserted (via surrounding whitespace).

Rules (as observed/documented by reverse-engineering and GitHub-compatible slug implementations):

  • Lowercase the heading text. [1]
  • Define “word” characters as letters/digits/underscore/hyphen; everything else (including emoji) counts as punctuation. [1]
  • Convert the heading into an anchor by:
    • Removing punctuation-only runs without spaces/tabs (no hyphen inserted). [1]
    • Replacing any run of spaces/tabs and/or punctuation that includes at least one space/tab with a single -. [1]

What that means for emoji (examples):

  • ## Hello 😄 World#hello-world (emoji dropped; spaces cause -). (Consistent with the rule above. [1])
  • ## Hello😄World#helloworld (emoji dropped; no spaces, so nothing becomes -). [1]
  • ## 😄 emoji#-emoji (leading emoji dropped; the following space becomes a leading -). [2]

Duplicates:

  • Repeated headings get -1, -2, ... appended to keep IDs unique (e.g., foo, then foo-1, then foo-2). [2]

Note on the actual HTML GitHub emits:

  • GitHub typically prefixes the generated anchor element’s id with user-content- (e.g. user-content-hello-world) while the permalink you use in Markdown/URLs is usually the unprefixed fragment #hello-world. [3]

Sources: [1] [2] [3]


Change #features anchor to #-features.

The heading ## ✨ Features generates the anchor #-features (not #features), because GitHub treats the emoji as punctuation; the space after the emoji produces a leading hyphen in the anchor slug. The current link will not navigate correctly.

Suggested fix
-[Features](`#features`) • [Quick Start](`#quick-start`) • [Configuration](`#configuration`)
+[Features](`#-features`) • [Quick Start](`#quick-start`) • [Configuration](`#configuration`)

The other two links (#quick-start and #configuration) are correct.

📝 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
[Features](#features)[Quick Start](#quick-start)[Configuration](#configuration)
[Features](`#-features`)[Quick Start](`#quick-start`)[Configuration](`#configuration`)
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 12-12: Link fragments should be valid

(MD051, link-fragments)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 12, The link text "[Features](`#features`) • [Quick
Start](`#quick-start`) • [Configuration](`#configuration`)" uses the wrong anchor
for the "## ✨ Features" heading; update the Features link to use the generated
slug by changing "(`#features`)" to "(`#-features`)" so the anchor matches the "## ✨
Features" heading and the link navigates correctly.

README.md Outdated
Comment on lines 37 to 44
## Quick Start

```
┌─────────────────────────────────────────────┐
│ Ready to launch your web server? │
│ Follow these simple steps! │
└─────────────────────────────────────────────┘
```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Several markdown lint issues flagged by static analysis.

  • Line 37: Extra space after ## in heading (## Quick Start## Quick Start).
  • Multiple fenced code blocks (Lines 39, 80, 87, 134, 234, 283, 340) lack a language identifier — add text, bash, etc. as appropriate for proper syntax highlighting and accessibility.

Also applies to: 39-43, 80-82, 87-89, 134-157, 234-239, 283-290, 340-342

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 37-37: Multiple spaces after hash on atx style heading

(MD019, no-multiple-space-atx)


[warning] 39-39: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 37 - 44, Remove the extra space in the level-2
heading so "##  Quick Start" becomes "## Quick Start", and add appropriate
fenced-code block language identifiers (e.g., text, bash) to each
triple-backtick block present in README.md—specifically the ASCII-art/server
launch block under the Quick Start heading and the other fenced blocks further
down (the examples and command snippets) so they render with proper syntax
highlighting and satisfy markdown lint rules; update only the heading text and
the opening backticks for the fenced blocks (no content changes).

README.md Outdated
This works across all Java versions and forms the foundation of most Java programs.
### 1. Clone the repository
```bash
git clone git clone https://github.com/ithsjava25/project-webserver-juv25g.git
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Duplicate git clone in the clone command.

-git clone git clone https://github.com/ithsjava25/project-webserver-juv25g.git
+git clone https://github.com/ithsjava25/project-webserver-juv25g.git
📝 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
git clone git clone https://github.com/ithsjava25/project-webserver-juv25g.git
git clone https://github.com/ithsjava25/project-webserver-juv25g.git
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 48, The README's repository clone command contains a
duplicated keyword ("git clone git clone ..."); update the command text to a
single correct invocation (e.g., "git clone
https://github.com/ithsjava25/project-webserver-juv25g.git") so the README shows
a valid clone command.

README.md Outdated
Comment on lines +329 to +336
### Filter Chain (Future Feature)

The project includes a filter chain interface for future extensibility:
- Request/response filtering
- Authentication
- Logging
- Compression

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

"Filter Chain (Future Feature)" is inaccurate — this PR adds a working RedirectFilter.

The README describes the filter chain as a future feature, but the PR itself introduces RedirectFilter, RedirectRule, and RedirectRulesLoader as working functionality. Update this section to document the redirect filter and its configuration, and mention that the filter chain is extensible for additional filters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 329 - 336, Update the README section that currently
labels "Filter Chain (Future Feature)" to document the new RedirectFilter,
RedirectRule, and RedirectRulesLoader: replace the "future feature" wording with
a brief description of RedirectFilter behavior, show how RedirectRule entries
are structured and loaded via RedirectRulesLoader, and mention the filter chain
remains extensible for adding more filters (e.g., authentication, logging,
compression); reference the RedirectFilter, RedirectRule, and
RedirectRulesLoader names so readers can find the implementations.

Comment on lines +19 to +29
public HttpRequest(String method,
String path,
String version,
Map<String, String> headers,
String body) {
this.method = method;
this.path = path;
this.version = version;
this.headers = headers != null ? Map.copyOf(headers) : Collections.emptyMap();
this.body = body;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find HttpRequest class and examine its structure
find . -name "HttpRequest.java" -type f

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 215


🏁 Script executed:

# Search for RedirectRule and RedirectFilter to understand the flow
rg -n --type=java "class RedirectRule|class RedirectFilter" -A 5

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 4989


🏁 Script executed:

# Check how HttpRequest is instantiated in the codebase
rg -n --type=java 'new HttpRequest\(' -B 2 -A 2

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 2151


🏁 Script executed:

# Find RedirectRule.matches() to see how it uses the path
rg -n --type=java "matches\(" --context=5 | grep -A 10 -B 5 "path\|Path"

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 8719


🏁 Script executed:

cat -n src/main/java/org/example/httpparser/HttpRequest.java

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 1592


🏁 Script executed:

# Check for `@NonNull/`@Nullable annotations in the httpparser package
rg -n "@NonNull|@Nullable|Objects.requireNonNull" src/main/java/org/example/httpparser/ -A 2 -B 2

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 90


🏁 Script executed:

# Check how the path is retrieved/accessed
rg -n "public.*path\(\)|getPath()" src/main/java/org/example/httpparser/ -A 3

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 463


🏁 Script executed:

# Check HttpParseRequestLine to see if it guarantees non-null path
cat -n src/main/java/org/example/httpparser/HttpParseRequestLine.java

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 2290


🏁 Script executed:

# Check for any tests or places where null path might be passed
rg -n "HttpRequest.*null\|new HttpRequest\(" -A 5 src/ | head -50

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 90


🏁 Script executed:

# Check if there's validation in the parser
rg -n "path\s*=\s*null\|path.*null" src/main/java/org/example/httpparser/ -B 3 -A 3

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 90


🏁 Script executed:

# Check the complete RedirectFilter.doFilter() method for null checks
rg -n "class RedirectFilter" src/main/java/org/example/ -A 30 | grep -A 30 "doFilter"

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 4569


🏁 Script executed:

# Check RedirectRule.matches() implementation completely
rg -n "public boolean matches" src/main/java/org/example/ -B 2 -A 5

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 1480


Add null validation for path in the HttpRequest constructor.

The constructor accepts a nullable path parameter without validation. Since RedirectFilter passes the path directly to RedirectRule.matches(), which calls Pattern.matcher(requestPath).matches(), a null path will cause NullPointerException. Add Objects.requireNonNull() for path, method, and version to enforce non-null contracts at construction time.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/httpparser/HttpRequest.java` around lines 19 - 29,
The HttpRequest constructor allows a null path which leads to NPE when
RedirectFilter/RedirectRule.matches() calls Pattern.matcher(requestPath). Add
null checks by calling Objects.requireNonNull(...) on path, method, and version
at the start of the HttpRequest constructor (and import java.util.Objects if
missing) so the constructor enforces non-null contracts and fails fast with a
clear message.

Comment on lines +58 to +66
public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
handleGetRequest(uri);
HttpResponseBuilder response = new HttpResponseBuilder();
response.setStatusCode(statusCode);
// Use MimeTypeDetector instead of hardcoded text/html
response.setContentTypeFromFilename(uri);
response.setBody(fileBytes);
outputStream.write(response.build());
outputStream.flush();
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd -type f -name "StaticFileHandler.java" -o -name "MimeTypeDetector.java" -o -name "StaticFileHandlerTest.java" | head -20

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 278


🏁 Script executed:

find . -name "StaticFileHandler.java" -o -name "MimeTypeDetector.java" -o -name "StaticFileHandlerTest.java"

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 268


🏁 Script executed:

cat -n ./src/main/java/org/example/StaticFileHandler.java

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 2818


🏁 Script executed:

cat -n ./src/main/java/org/example/http/MimeTypeDetector.java

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 3201


🏁 Script executed:

cat -n ./src/test/java/org/example/StaticFileHandlerTest.java

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 6628


🏁 Script executed:

cat -n ./src/main/java/org/example/http/HttpResponseBuilder.java

Repository: ithsjava25/ithsjava25-java-project-webserver-maven-java-template

Length of output: 4509


Content-Type is derived from the unsanitized URI — MIME detection breaks for URIs with query strings or fragments.

sendGetRequest passes the original uri (e.g., "index.html?foo=bar") to setContentTypeFromFilename. Since handleGetRequest sanitizes only its local uri variable without modifying the method parameter, the unsanitized string reaches MimeTypeDetector.detectMimeType(), which extracts the extension as ".html?foo=bar". This won't match any MIME type in the map, defaulting to application/octet-stream instead of text/html; charset=UTF-8.

The test sanitized_uris_should_return_200 passes URIs with query strings but only asserts the status code, not the Content-Type header, leaving this bug undetected.

Proposed fix — sanitize uri before MIME detection
     public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
+        // Sanitize for Content-Type detection (strip query/fragment)
+        String sanitized = uri;
+        int q = sanitized.indexOf('?');
+        if (q >= 0) sanitized = sanitized.substring(0, q);
+        int h = sanitized.indexOf('#');
+        if (h >= 0) sanitized = sanitized.substring(0, h);
+
         handleGetRequest(uri);
         HttpResponseBuilder response = new HttpResponseBuilder();
         response.setStatusCode(statusCode);
-        response.setContentTypeFromFilename(uri);
+        response.setContentTypeFromFilename(sanitized);
         response.setBody(fileBytes);
         outputStream.write(response.build());
         outputStream.flush();
     }

A cleaner approach would be to have handleGetRequest return a result object or store the sanitized URI as a field to avoid duplication.

📝 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
public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
handleGetRequest(uri);
HttpResponseBuilder response = new HttpResponseBuilder();
response.setStatusCode(statusCode);
// Use MimeTypeDetector instead of hardcoded text/html
response.setContentTypeFromFilename(uri);
response.setBody(fileBytes);
outputStream.write(response.build());
outputStream.flush();
public void sendGetRequest(OutputStream outputStream, String uri) throws IOException {
// Sanitize for Content-Type detection (strip query/fragment)
String sanitized = uri;
int q = sanitized.indexOf('?');
if (q >= 0) sanitized = sanitized.substring(0, q);
int h = sanitized.indexOf('#');
if (h >= 0) sanitized = sanitized.substring(0, h);
handleGetRequest(uri);
HttpResponseBuilder response = new HttpResponseBuilder();
response.setStatusCode(statusCode);
response.setContentTypeFromFilename(sanitized);
response.setBody(fileBytes);
outputStream.write(response.build());
outputStream.flush();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/StaticFileHandler.java` around lines 58 - 66,
sendGetRequest currently passes the raw uri to setContentTypeFromFilename,
causing MimeTypeDetector.detectMimeType to see query strings/fragments (e.g.,
".html?foo=bar") and return wrong MIME; sanitize/normalize the URI before MIME
detection by reusing the sanitized value produced in handleGetRequest (or have
handleGetRequest return the sanitized path or store it on the instance), then
call setContentTypeFromFilename with that sanitized filename/path (ensuring
query string and fragment are removed) so Content-Type is derived from the
cleaned filename rather than the original uri.

Comment on lines +32 to +34
} catch (Exception e) {
throw new RuntimeException("Error handling client connection " + e);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Original exception stack trace is lost — use the two-arg RuntimeException constructor.

"Error handling client connection " + e stringifies the exception. The original cause and its stack trace are discarded, making debugging much harder.

Proposed fix
-            throw new RuntimeException("Error handling client connection " + e);
+            throw new RuntimeException("Error handling client connection", e);
📝 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
} catch (Exception e) {
throw new RuntimeException("Error handling client connection " + e);
}
} catch (Exception e) {
throw new RuntimeException("Error handling client connection", e);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/TcpServer.java` around lines 32 - 34, The catch
block in TcpServer currently throws a RuntimeException by concatenating the
caught exception into the message, which discards the original stack trace;
change the throw to use the two-argument constructor so the caught exception
becomes the cause (e.g., replace the existing throw in the client-handling catch
with new RuntimeException("Error handling client connection", e)) to preserve
the original exception and stack trace.

Comment on lines +269 to +284
// Unknown status codes
@ParameterizedTest
@ValueSource(ints = {999, 123, 777, 100, 600})
@DisplayName("Should handle unknown status codes gracefully")
void build_handlesUnknownStatusCodes(int statusCode) {
HttpResponseBuilder builder = new HttpResponseBuilder();
builder.setStatusCode(statusCode);
builder.setBody("");

byte[] result = builder.build();
String resultStr = asString(result);

assertThat(resultStr)
.startsWith("HTTP/1.1 " + statusCode)
.doesNotContain("OK");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Status code 100 is a valid HTTP status code (Continue), not "unknown".

The test is titled "Should handle unknown status codes gracefully" but 100 is a standard HTTP status. The test may still pass (the builder likely won't emit "OK" for it), but it misrepresents the intent. Consider replacing 100 with a truly non-standard code (e.g., 299 or 499).

-    `@ValueSource`(ints = {999, 123, 777, 100, 600})
+    `@ValueSource`(ints = {999, 123, 777, 299, 600})
📝 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
// Unknown status codes
@ParameterizedTest
@ValueSource(ints = {999, 123, 777, 100, 600})
@DisplayName("Should handle unknown status codes gracefully")
void build_handlesUnknownStatusCodes(int statusCode) {
HttpResponseBuilder builder = new HttpResponseBuilder();
builder.setStatusCode(statusCode);
builder.setBody("");
byte[] result = builder.build();
String resultStr = asString(result);
assertThat(resultStr)
.startsWith("HTTP/1.1 " + statusCode)
.doesNotContain("OK");
}
// Unknown status codes
`@ParameterizedTest`
`@ValueSource`(ints = {999, 123, 777, 299, 600})
`@DisplayName`("Should handle unknown status codes gracefully")
void build_handlesUnknownStatusCodes(int statusCode) {
HttpResponseBuilder builder = new HttpResponseBuilder();
builder.setStatusCode(statusCode);
builder.setBody("");
byte[] result = builder.build();
String resultStr = asString(result);
assertThat(resultStr)
.startsWith("HTTP/1.1 " + statusCode)
.doesNotContain("OK");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/http/HttpResponseBuilderTest.java` around lines 269
- 284, The test build_handlesUnknownStatusCodes in HttpResponseBuilderTest
mistakenly treats 100 as an "unknown" status; update the ValueSource in that
parameterized test so it uses only non-standard codes (e.g., replace 100 with
299 or 499) to reflect the intent, keeping the test method name and assertions
the same; locate the ValueSource ints array in build_handlesUnknownStatusCodes
and swap the 100 entry for a truly non-standard code.

Comment on lines +15 to +28
@Test
void TestHttpParserForHeaders() throws IOException {
String testInput = "GET /index.html HTTP/1.1\r\nHost: localhost\r\nContent-Type: text/plain\r\nUser-Agent: JUnit5\r\n\r\n";
InputStream in = new ByteArrayInputStream(testInput.getBytes(StandardCharsets.UTF_8));

httpParser.setReader(in);
httpParser.parseHttp();

assertNotNull(httpParser.getHeadersMap());
assertThat(httpParser.getHeadersMap().size()).isEqualTo(3);
assertThat(httpParser.getHeadersMap().get("Host")).contains("localhost");
assertThat(httpParser.getHeadersMap().get("Content-Type")).contains("text/plain");
assertThat(httpParser.getHeadersMap().get("User-Agent")).contains("JUnit5");
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test parses headers without first consuming the request line.

parseHttp() is called without a preceding parseRequest() call. The request line "GET /index.html HTTP/1.1" is fed to the header parser and happens to be skipped because it has no colon—but this is accidental. A proper test should call parseRequest() first (as the real usage flow would), then parseHttp(), so the request line is consumed before header parsing begins. Otherwise this test silently breaks if the request line format ever contains a colon.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/httpparser/HttpParserTest.java` around lines 15 -
28, The test TestHttpParserForHeaders feeds a full HTTP request but calls
parseHttp() directly, so the request line ("GET /index.html...") is not consumed
properly; update the test to first call httpParser.parseRequest() after setting
the input stream (httpParser.setReader(...)) and then call
httpParser.parseHttp() so the request line is consumed by parseRequest() before
header parsing begins (adjust TestHttpParserForHeaders to invoke parseRequest()
then parseHttp()).

Copy link

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/example/filter/redirect/RedirectFilter.java`:
- Line 33: The Location header is set directly from rule.getTargetUrl() without
stripping CR/LF, enabling HTTP response splitting; update the code that sets the
header in RedirectFilter (the response.setHeader("Location",
rule.getTargetUrl()) call) to sanitize the target URL by removing or replacing
CR and LF characters (same approach used for path.replaceAll("[\\r\\n]", "_")),
or ensure RedirectRule's constructor/validator strips CR/LF from its targetUrl;
locate RedirectRule and add equivalent replaceAll("[\\r\\n]", "_") sanitization
on targetUrl at construction time if not already present, or sanitize inside the
RedirectFilter before calling response.setHeader.
- Around line 29-36: Restore the missing guard so the redirect only runs when a
rule matches: wrap the existing redirect logic inside an if (rule.matches(path))
{ ... } block in RedirectFilter (the for loop iterating RedirectRule rule :
rules), ensuring the opening and closing braces are balanced so
chain.doFilter(request, response) remains inside doFilter and the method
compiles; keep the sanitizedPath/logging, response.setStatusCode(...),
response.setHeader("Location", ...), and return; only executed when
rule.matches(path) is true.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between aebd6de and 5fdac73.

📒 Files selected for processing (1)
  • src/main/java/org/example/filter/redirect/RedirectFilter.java

final String sanitizedPath = path.replaceAll("[\\r\\n]", "_");
LOG.info(() -> "Redirecting " + sanitizedPath + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")");
response.setStatusCode(rule.getStatusCode());
response.setHeader("Location", rule.getTargetUrl());
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

targetUrl set in Location header without CRLF sanitization — potential HTTP response splitting

The request path is sanitized (path.replaceAll("[\\r\\n]", "_")), but rule.getTargetUrl() is injected directly into the Location header with no equivalent stripping. If RedirectRule does not validate or sanitize the URL on construction, an attacker who can influence redirect targets could inject arbitrary headers.

Verify whether RedirectRule strips CR/LF from targetUrl at construction time:

#!/bin/bash
# Locate RedirectRule and check for CR/LF handling in targetUrl
fd -e java RedirectRule --exec rg -n "targetUrl|replaceAll|\\\\r|\\\\n|CRLF|sanitize" {}

If no sanitization is present, apply the same guard used for path:

🛡️ Proposed fix — sanitize targetUrl before setting the header
-                response.setHeader("Location", rule.getTargetUrl());
+                response.setHeader("Location", rule.getTargetUrl().replaceAll("[\\r\\n]", "_"));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/redirect/RedirectFilter.java` at line 33,
The Location header is set directly from rule.getTargetUrl() without stripping
CR/LF, enabling HTTP response splitting; update the code that sets the header in
RedirectFilter (the response.setHeader("Location", rule.getTargetUrl()) call) to
sanitize the target URL by removing or replacing CR and LF characters (same
approach used for path.replaceAll("[\\r\\n]", "_")), or ensure RedirectRule's
constructor/validator strips CR/LF from its targetUrl; locate RedirectRule and
add equivalent replaceAll("[\\r\\n]", "_") sanitization on targetUrl at
construction time if not already present, or sanitize inside the RedirectFilter
before calling response.setHeader.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (4)
src/main/java/org/example/filter/redirect/RedirectRulesLoader.java (3)

15-15: Use strip() consistently instead of trim().

Line 15 uses trim() while line 18 uses strip(). trim() only removes ASCII control characters (<= '\u0020'), whereas strip() is Unicode-aware and is the modern Java replacement.

♻️ Proposed fix
-        String trimmed = sourcePath.trim();
+        String trimmed = sourcePath.strip();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/redirect/RedirectRulesLoader.java` at line
15, In RedirectRulesLoader replace the use of String trimmed = sourcePath.trim()
with the Unicode-aware strip variant to match the rest of the class (use
sourcePath.strip()); update any related variables or usages of trimmed if needed
so the code consistently uses strip() instead of trim() across the class/method
that handles sourcePath.

34-45: Batch consecutive literal characters into a single Pattern.quote call.

The current loop calls Pattern.quote(String.valueOf(c)) for every non-wildcard character individually, producing verbose output like \Q/\E\Qa\E\Qp\E\Qi\E instead of \Q/api\E. Collecting runs of literal characters and quoting them in one call is cleaner and avoids redundant open/close \Q…\E pairs.

♻️ Proposed refactor
-    private static String wildcardToRegex(String wildcard) {
-        StringBuilder sb = new StringBuilder();
-        for (int i = 0; i < wildcard.length(); i++) {
-            char c = wildcard.charAt(i);
-            if (c == '*') {
-                sb.append("[^/]*");
-            } else {
-                sb.append(Pattern.quote(String.valueOf(c)));
-            }
-        }
-        return sb.toString();
-    }
+    private static String wildcardToRegex(String wildcard) {
+        StringBuilder sb = new StringBuilder();
+        StringBuilder literal = new StringBuilder();
+        for (int i = 0; i < wildcard.length(); i++) {
+            char c = wildcard.charAt(i);
+            if (c == '*') {
+                if (!literal.isEmpty()) {
+                    sb.append(Pattern.quote(literal.toString()));
+                    literal.setLength(0);
+                }
+                sb.append("[^/]*");
+            } else {
+                literal.append(c);
+            }
+        }
+        if (!literal.isEmpty()) {
+            sb.append(Pattern.quote(literal.toString()));
+        }
+        return sb.toString();
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/redirect/RedirectRulesLoader.java` around
lines 34 - 45, The wildcardToRegex method currently quotes each non-wildcard
character individually; refactor wildcardToRegex to batch consecutive literal
characters into a single substring and call Pattern.quote once per run: iterate
the input, accumulate a start index (or StringBuilder) for literal runs, when
you hit '*' flush the accumulated literal via Pattern.quote(sb.toString()) to
the main StringBuilder, append "[^/]*" for '*' and continue, and after the loop
flush any remaining literal run; this produces fewer \Q...\E segments while
preserving the existing '*' → "[^/]*" behavior.

17-23: Raw regex branch silently omits anchors; consider adding them for parity.

The literal and wildcard branches both wrap their result with ^...$, but the regex: branch does not. This is not a functional bug today — RedirectRule.matches() uses Matcher.matches() which anchors the full input implicitly. However, if the call site is ever changed to Matcher.find(), the raw regex branch would silently change semantics to substring matching while the other branches continue to anchor. Wrapping raw input with anchors (or at least documenting the omission) makes the three branches consistent and resilient to future changes.

♻️ Proposed fix
-            return Pattern.compile(rawRegex);
+            return Pattern.compile("^(?:" + rawRegex + ")$");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/redirect/RedirectRulesLoader.java` around
lines 17 - 23, The regex branch in RedirectRulesLoader currently compiles
rawRegex without anchors, making it inconsistent with the literal/wildcard
branches that wrap results with ^...$; change the Pattern.compile(rawRegex) call
to compile("^" + rawRegex + "$") so the raw regex branch is anchored the same
way (keep the existing blank check for rawRegex and use REGEX_PREFIX/rawRegex as
the referenced symbols).
src/main/java/org/example/filter/redirect/RedirectRule.java (1)

17-19: Consider supporting 307 and 308 redirect codes.

The constraint statusCode != 301 && statusCode != 302 rules out 307 (Temporary Redirect) and 308 (Permanent Redirect), both of which are widely used standard codes. If the server is intended to evolve beyond a simple redirect filter, accepting all semantically valid redirect codes (301, 302, 303, 307, 308) avoids a follow-up breaking change.

♻️ Proposed fix
-        if (statusCode != 301 && statusCode != 302) {
-            throw new IllegalArgumentException("statusCode must be 301 or 302");
+        if (statusCode != 301 && statusCode != 302 && statusCode != 303
+                && statusCode != 307 && statusCode != 308) {
+            throw new IllegalArgumentException("statusCode must be a valid redirect code (301, 302, 303, 307, or 308)");
        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/redirect/RedirectRule.java` around lines 17
- 19, The constructor/validation in RedirectRule currently rejects any status
code other than 301 or 302; update the validation logic in RedirectRule (the
statusCode check/throw) to allow the full set of standard redirect codes (301,
302, 303, 307, 308) and update the IllegalArgumentException message accordingly;
locate the check that compares statusCode (the if block throwing
IllegalArgumentException) and change the condition to reject only values not in
{301,302,303,307,308} and ensure the exception text lists the supported codes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/org/example/filter/redirect/RedirectRule.java`:
- Around line 27-29: The matches() method currently calls
sourcePattern.matcher(requestPath) which will throw a NullPointerException if
requestPath is null; update matches(String requestPath) in RedirectRule to guard
against null by returning false when requestPath is null (i.e., check
requestPath == null before calling sourcePattern.matcher(...).matches()),
ensuring sourcePattern is still used only when requestPath is non-null.

---

Nitpick comments:
In `@src/main/java/org/example/filter/redirect/RedirectRule.java`:
- Around line 17-19: The constructor/validation in RedirectRule currently
rejects any status code other than 301 or 302; update the validation logic in
RedirectRule (the statusCode check/throw) to allow the full set of standard
redirect codes (301, 302, 303, 307, 308) and update the IllegalArgumentException
message accordingly; locate the check that compares statusCode (the if block
throwing IllegalArgumentException) and change the condition to reject only
values not in {301,302,303,307,308} and ensure the exception text lists the
supported codes.

In `@src/main/java/org/example/filter/redirect/RedirectRulesLoader.java`:
- Line 15: In RedirectRulesLoader replace the use of String trimmed =
sourcePath.trim() with the Unicode-aware strip variant to match the rest of the
class (use sourcePath.strip()); update any related variables or usages of
trimmed if needed so the code consistently uses strip() instead of trim() across
the class/method that handles sourcePath.
- Around line 34-45: The wildcardToRegex method currently quotes each
non-wildcard character individually; refactor wildcardToRegex to batch
consecutive literal characters into a single substring and call Pattern.quote
once per run: iterate the input, accumulate a start index (or StringBuilder) for
literal runs, when you hit '*' flush the accumulated literal via
Pattern.quote(sb.toString()) to the main StringBuilder, append "[^/]*" for '*'
and continue, and after the loop flush any remaining literal run; this produces
fewer \Q...\E segments while preserving the existing '*' → "[^/]*" behavior.
- Around line 17-23: The regex branch in RedirectRulesLoader currently compiles
rawRegex without anchors, making it inconsistent with the literal/wildcard
branches that wrap results with ^...$; change the Pattern.compile(rawRegex) call
to compile("^" + rawRegex + "$") so the raw regex branch is anchored the same
way (keep the existing blank check for rawRegex and use REGEX_PREFIX/rawRegex as
the referenced symbols).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5fdac73 and f40e141.

📒 Files selected for processing (5)
  • pom.xml
  • src/main/java/org/example/filter/redirect/RedirectFilter.java
  • src/main/java/org/example/filter/redirect/RedirectRule.java
  • src/main/java/org/example/filter/redirect/RedirectRulesLoader.java
  • src/test/java/org/example/filter/redirect/RedirectFilterTest.java
💤 Files with no reviewable changes (1)
  • pom.xml
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/main/java/org/example/filter/redirect/RedirectFilter.java
  • src/test/java/org/example/filter/redirect/RedirectFilterTest.java

Comment on lines +27 to +29
public boolean matches(String requestPath) {
return sourcePattern.matcher(requestPath).matches();
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

matches() will throw NPE when requestPath is null.

Pattern.matcher(null) throws NullPointerException. If any upstream component passes a null path (e.g., a malformed HTTP request where the path could not be parsed), the exception will bubble up the filter chain uncaught instead of cleanly returning false.

🛡️ Proposed fix
     public boolean matches(String requestPath) {
-        return sourcePattern.matcher(requestPath).matches();
+        if (requestPath == null) return false;
+        return sourcePattern.matcher(requestPath).matches();
     }
📝 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
public boolean matches(String requestPath) {
return sourcePattern.matcher(requestPath).matches();
}
public boolean matches(String requestPath) {
if (requestPath == null) return false;
return sourcePattern.matcher(requestPath).matches();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/redirect/RedirectRule.java` around lines 27
- 29, The matches() method currently calls sourcePattern.matcher(requestPath)
which will throw a NullPointerException if requestPath is null; update
matches(String requestPath) in RedirectRule to guard against null by returning
false when requestPath is null (i.e., check requestPath == null before calling
sourcePattern.matcher(...).matches()), ensuring sourcePattern is still used only
when requestPath is non-null.

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.

5 participants