Skip to content

Server‑side caching Issue36#66

Open
Boppler12 wants to merge 39 commits intomainfrom
issue36
Open

Server‑side caching Issue36#66
Boppler12 wants to merge 39 commits intomainfrom
issue36

Conversation

@Boppler12
Copy link

@Boppler12 Boppler12 commented Feb 18, 2026

Summary by CodeRabbit

  • New Features

    • Static file caching for improved performance on repeated requests.
    • URI sanitization and security checks for path handling.
    • Public cache clearing API.
  • Bug Fixes

    • Standardized HTTP error responses for safer and consistent handling.
  • Tests

    • Added comprehensive tests covering caching, concurrent reads, and edge cases including null-byte injection and query string handling.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 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 refactors the static file handler to use constructor-based configuration with a default web root, introduces an in-memory file cache, sanitizes incoming URIs, validates against path traversal attacks, and reorganizes request handling into a stateless, cache-backed flow with standardized error responses.

Changes

Cohort / File(s) Summary
Cache Infrastructure
src/main/java/org/example/FileCache.java
New FileCache class providing thread-safe in-memory caching with static get/put/clear methods and ConcurrentHashMap backing.
Static File Handler Refactoring
src/main/java/org/example/StaticFileHandler.java
Added two constructors (default and parameterized with webRoot), introduced public sendGetRequest method with URI sanitization, path traversal validation, cache-backed file loading, and standardized error responses via new helper methods.
Test Suite Expansion
src/test/java/org/example/StaticFileHandlerTest.java
Added test infrastructure with @TempDir and helper methods, new test cases covering caching behavior, URI sanitization edge cases (query strings, leading slashes, null bytes), and concurrent multi-threaded reads.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant StaticFileHandler
    participant FileCache
    participant Disk

    Client->>StaticFileHandler: sendGetRequest(outputStream, uri)
    StaticFileHandler->>StaticFileHandler: sanitizeUri(uri)
    StaticFileHandler->>StaticFileHandler: isPathTraversal(sanitized)
    
    alt Path Traversal Detected
        StaticFileHandler->>StaticFileHandler: writeResponse(403, "Forbidden")
        StaticFileHandler->>Client: 403 Response
    else Safe Path
        StaticFileHandler->>FileCache: get(cacheKey)
        
        alt Cache Hit
            FileCache-->>StaticFileHandler: byte[] content
        else Cache Miss
            StaticFileHandler->>Disk: readFile(webRoot, sanitized)
            Disk-->>StaticFileHandler: byte[] content
            StaticFileHandler->>FileCache: put(cacheKey, content)
        end
        
        alt File Found
            StaticFileHandler->>StaticFileHandler: buildResponse(200, content, contentType)
            StaticFileHandler->>Client: 200 + File Content
        else File Not Found
            StaticFileHandler->>StaticFileHandler: writeResponse(404, "Not Found")
            StaticFileHandler->>Client: 404 Response
        end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A cache to store our files with care,
Paths checked for travails in the air,
Queries trimmed and slashes tamed,
Concurrent threads no longer blamed—
Swift responses hop back with flair! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.77% 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 'Server‑side caching Issue36' directly references the primary feature being implemented (server-side caching) and the issue number, clearly summarizing the main change.

✏️ 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
  • Commit unit tests in branch issue36

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: 4

🧹 Nitpick comments (3)
src/main/java/org/example/StaticFileHandler.java (1)

21-21: Consider replacing System.out.println with a proper logger.

System.out.println on hot request paths adds synchronization overhead (the underlying PrintStream is synchronized) and bypasses any logging configuration. A java.util.logging.Logger or SLF4J Logger is the idiomatic alternative.

Also applies to: 25-25

🤖 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` at line 21, Replace the two
System.out.println calls in StaticFileHandler with a proper logger: add a static
final Logger (e.g., java.util.logging.Logger or org.slf4j.Logger) on the
StaticFileHandler class, initialize it with the class name, and change the
println statements (the "✓ Cache hit for: " and the other occurrence) to
appropriate logger calls (info/debug) using parameterized or concatenated
messages; remove the println usages so logging honours configuration and avoids
PrintStream synchronization overhead.
src/main/java/org/example/FileCache.java (1)

7-7: Unbounded heap growth — no eviction or size cap.

Every unique URI populates the cache permanently. A server with many files or long uptime will accumulate all file bytes in heap with no release path. Consider a bounded LRU structure (e.g. LinkedHashMap with removeEldestEntry override, or Guava CacheBuilder.maximumSize()) to set a ceiling and evict least-recently-used entries.

Also applies to: 24-26

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

In `@src/main/java/org/example/FileCache.java` at line 7, The cache field in
FileCache is an unbounded HashMap (private final Map<String, byte[]> cache)
causing uncontrolled heap growth; replace it with a bounded LRU cache
implementation (for example, use a LinkedHashMap subclass with accessOrder=true
and override removeEldestEntry to enforce a max entries limit, or replace with a
Guava Cache created via CacheBuilder.maximumSize(n)) and apply the same change
to the related methods that read/write the cache (e.g., put/get/remove logic
used around lines referenced) so entries are evicted when the size limit is
exceeded and memory is bounded.
src/test/java/org/example/FileCacheTest.java (1)

7-116: Well-structured test suite — good coverage of the public API.

All nine cases (round-trip, missing-key null, overwrite semantics, clear, size progression, large/empty payloads) are clear and independently isolated via @BeforeEach. A couple of optional additions would strengthen the suite:

  1. Aliasing / defensive-copy test — verify that mutating the array returned by get does not change what the cache subsequently returns (this would simultaneously document and catch the mutable-aliasing issue flagged in FileCache):
`@Test`
void testGetReturnIsIndependentOfCache() {
    byte[] original = "hello".getBytes();
    cache.put("file.txt", original);
    byte[] fetched = cache.get("file.txt");
    fetched[0] = 'X';  // mutate the returned copy
    assertThat(cache.get("file.txt")).isEqualTo("hello".getBytes());
}
  1. Concurrent-access test — a simple latch-based test with two threads doing concurrent puts/gets would confirm thread-safety once ConcurrentHashMap is adopted.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/FileCacheTest.java` around lines 7 - 116, Add two
tests to FileCacheTest: one named testGetReturnIsIndependentOfCache that
verifies get returns a defensive copy by mutating the fetched byte[] and
asserting subsequent cache.get("file.txt") still equals the original bytes; and
one simple concurrency test (e.g., testConcurrentPutAndGet) that uses a
CountDownLatch and two threads to perform concurrent cache.put and cache.get
calls to exercise thread-safety (use FileCache instance, assert final
size/contents as expected). Ensure both tests are annotated with `@Test` and use
the existing cache field initialized in setUp().
🤖 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/FileCache.java`:
- Line 7: The cache field uses a non-thread-safe HashMap and stores mutable
byte[] references; replace the Map implementation with a thread-safe
ConcurrentHashMap for the field (FileCache.cache) and make defensive copies of
byte arrays on entry and exit: when storing (e.g., FileCache.put or wherever
cache.put is called) copy the incoming byte[] (Arrays.copyOf) before placing it
into the map, and when returning data (e.g., FileCache.get) return a copy of the
stored byte[] rather than the internal reference; this prevents concurrent
corruption and aliasing of mutable arrays.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Line 14: The instance field fileBytes in StaticFileHandler causes a data race
between concurrent requests; change handleGetRequest to return a byte[] instead
of writing to the instance field and remove the fileBytes field entirely, then
update sendGetRequest to call handleGetRequest and store the returned byte[] in
a local variable before using it to write the response; ensure all references to
the instance field (fileBytes) are replaced with the local variable and adjust
method signatures for handleGetRequest accordingly.
- Around line 26-27: The code creates File file = new File(WEB_ROOT, uri) and
reads it directly (Files.readAllBytes) allowing path traversal; fix by
canonical-path jail: resolve both WEB_ROOT and the resolved file via
getCanonicalPath() (and URL-decode/normalize uri beforehand), then verify the
fileCanonicalPath startsWith webRootCanonicalPath; if not, reject the request
(return 403 or equivalent) instead of calling Files.readAllBytes and caching
fileBytes. Apply this check in the StaticFileHandler where File file, fileBytes,
and Files.readAllBytes are used.
- Line 15: The FileCache instance is currently an instance field on
StaticFileHandler (private final FileCache cache = new FileCache()) so a new
empty cache is created per connection; change it to a shared cache so it
survives requests by making the field static final (e.g. private static final
FileCache CACHE = new FileCache()) or accept a FileCache via constructor
injection from the server so ConnectionHandler.runConnectionHandler() (invoked
by TcpServer.handleClient()) reuses the same cache; update all references to the
instance field to use the new static field name or the injected field and ensure
thread-safety in FileCache if needed.

---

Nitpick comments:
In `@src/main/java/org/example/FileCache.java`:
- Line 7: The cache field in FileCache is an unbounded HashMap (private final
Map<String, byte[]> cache) causing uncontrolled heap growth; replace it with a
bounded LRU cache implementation (for example, use a LinkedHashMap subclass with
accessOrder=true and override removeEldestEntry to enforce a max entries limit,
or replace with a Guava Cache created via CacheBuilder.maximumSize(n)) and apply
the same change to the related methods that read/write the cache (e.g.,
put/get/remove logic used around lines referenced) so entries are evicted when
the size limit is exceeded and memory is bounded.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Line 21: Replace the two System.out.println calls in StaticFileHandler with a
proper logger: add a static final Logger (e.g., java.util.logging.Logger or
org.slf4j.Logger) on the StaticFileHandler class, initialize it with the class
name, and change the println statements (the "✓ Cache hit for: " and the other
occurrence) to appropriate logger calls (info/debug) using parameterized or
concatenated messages; remove the println usages so logging honours
configuration and avoids PrintStream synchronization overhead.

In `@src/test/java/org/example/FileCacheTest.java`:
- Around line 7-116: Add two tests to FileCacheTest: one named
testGetReturnIsIndependentOfCache that verifies get returns a defensive copy by
mutating the fetched byte[] and asserting subsequent cache.get("file.txt") still
equals the original bytes; and one simple concurrency test (e.g.,
testConcurrentPutAndGet) that uses a CountDownLatch and two threads to perform
concurrent cache.put and cache.get calls to exercise thread-safety (use
FileCache instance, assert final size/contents as expected). Ensure both tests
are annotated with `@Test` and use the existing cache field initialized in
setUp().

import java.util.Map;

public class FileCache {
private final Map<String, byte []> cache = new HashMap<>();
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

Thread-unsafe HashMap + mutable byte-array aliasing will corrupt the cache under concurrent load.

Two distinct problems compound each other:

  1. HashMap is not thread-safe. A web server dispatches requests on multiple threads; concurrent put/get/contains calls can trigger a resize loop, return null for existing keys, or silently lose entries.
  2. put stores the caller's array reference as-is, and get returns the internal reference directly. Any caller that later overwrites the returned array also overwrites the cached content.
🔒 Proposed fix — ConcurrentHashMap + defensive copies
-import java.util.HashMap;
-import java.util.Map;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;

 public class FileCache {
-    private final Map<String, byte []> cache = new HashMap<>();
+    private final Map<String, byte[]> cache = new ConcurrentHashMap<>();

     public byte[] get(String key) {
-        return cache.get(key);
+        byte[] value = cache.get(key);
+        return value != null ? value.clone() : null;
     }

     public void put(String key, byte[] value) {
-        cache.put(key, value);
+        cache.put(key, value.clone());
     }

Also applies to: 13-15, 17-19

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

In `@src/main/java/org/example/FileCache.java` at line 7, The cache field uses a
non-thread-safe HashMap and stores mutable byte[] references; replace the Map
implementation with a thread-safe ConcurrentHashMap for the field
(FileCache.cache) and make defensive copies of byte arrays on entry and exit:
when storing (e.g., FileCache.put or wherever cache.put is called) copy the
incoming byte[] (Arrays.copyOf) before placing it into the map, and when
returning data (e.g., FileCache.get) return a copy of the stored byte[] rather
than the internal reference; this prevents concurrent corruption and aliasing of
mutable arrays.

Copy link

@viktorlindell12 viktorlindell12 left a comment

Choose a reason for hiding this comment

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

Bra jobbat.
Koden är tydlig och lätt att följa, bra uppdelning mellan de olika delarna.

Just nu konverteras byte[] till String i build(). Detta kan bli problem senare om servern ska hantera binära filer, till exempel bilder eller PDF:er. Ett alternativ kan vara att behålla body som byte[] hela vägen och skriva ut bytes direkt till output-streamen istället.

@kappsegla
Copy link

What do you think about converting this into a Filter instead of changing StaticFileHandler?

The reason for this would be that we can enable/disable cache by adding or removing the filter and it will also work for content not returned from StaticFileHandler in the future?

@eeebbaandersson eeebbaandersson linked an issue Feb 19, 2026 that may be closed by this pull request
@donne41 donne41 self-requested a review February 20, 2026 13:33
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 (1)
src/main/java/org/example/CacheFilter.java (1)

10-13: Replace System.out.println with a proper logging framework.

Bare System.out.println with Unicode symbols is not suitable for production. Use java.util.logging or SLF4J at an appropriate level (e.g., FINE/DEBUG for cache diagnostics). This also avoids I/O contention on stdout under load.

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

In `@src/main/java/org/example/CacheFilter.java` around lines 10 - 13, In
CacheFilter, replace the two System.out.println calls around the cache.get(uri)
return (the "✓ Cache hit for" and "✗ Cache miss for" prints) with a proper
logger: add a private static final org.slf4j.Logger (or
java.util.logging.Logger) instance at class scope and log the messages at
DEBUG/FINE level (e.g., logger.debug("Cache hit for: {}", uri) /
logger.debug("Cache miss for: {}", uri)); remove Unicode glyphs or keep them
only if your logging policy allows, and update imports accordingly so you no
longer use System.out.println in this class.
🤖 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/CacheFilter.java`:
- Around line 5-6: The current CacheFilter creates an unbounded FileCache
(private final FileCache cache = new FileCache()) which can leak memory; update
the cache to enforce size/TTL eviction. Replace or change the FileCache
instantiation in CacheFilter to use a bounded cache implementation (e.g., a
LinkedHashMap-based LRU with removeEldestEntry, or integrate Caffeine/Guava and
call Caffeine.newBuilder().maximumSize(...).expireAfterAccess(...).build()) and
ensure CacheFilter uses that bounded instance for get/put operations;
alternatively, add eviction logic inside FileCache (maxSize and
expireAfterAccess/expireAfterWrite) so CacheFilter keeps the same field but the
underlying implementation enforces limits.

---

Duplicate comments:
In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 17-19: The lambda passed to cacheFilter.getOrFetch uses the raw
user-controlled path and creates new File(WEB_ROOT, path) without
canonicalization or containment checks; canonicalize both WEB_ROOT and the
resolved file (via File.getCanonicalPath or similar) inside the lambda used by
cacheFilter.getOrFetch, verify the resolved path startsWith the canonical
WEB_ROOT path, and if it does not throw/return a safe error (e.g.,
403/IllegalArgument) rather than reading the file; update the lambda referenced
in cacheFilter.getOrFetch, avoid using the raw path directly with
Files.readAllBytes, and ensure the check prevents traversal before calling
Files.readAllBytes.
- Line 14: The cache is currently instance-scoped (private final CacheFilter
cacheFilter in StaticFileHandler) so each request gets a fresh FileCache; change
this to a shared cache by making the CacheFilter or its underlying FileCache
static final or by wiring a singleton/shared CacheFilter into StaticFileHandler
(e.g., accept a CacheFilter in the constructor and have
ConnectionHandler.runConnectionHandler() pass a long-lived/shared instance).
Update the field declaration and constructor usages to use the shared instance
and remove per-request instantiation so cached entries survive across requests.

---

Nitpick comments:
In `@src/main/java/org/example/CacheFilter.java`:
- Around line 10-13: In CacheFilter, replace the two System.out.println calls
around the cache.get(uri) return (the "✓ Cache hit for" and "✗ Cache miss for"
prints) with a proper logger: add a private static final org.slf4j.Logger (or
java.util.logging.Logger) instance at class scope and log the messages at
DEBUG/FINE level (e.g., logger.debug("Cache hit for: {}", uri) /
logger.debug("Cache miss for: {}", uri)); remove Unicode glyphs or keep them
only if your logging policy allows, and update imports accordingly so you no
longer use System.out.println in this class.

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: 4

🧹 Nitpick comments (2)
src/main/java/org/example/http/HttpResponseBuilder.java (2)

45-48: setBody(byte[]) does not defensively copy the input array.

If a caller mutates the byte[] after calling setBody, the built response will silently contain the mutated data. Consider cloning defensively, especially since this handles binary file content that may come from a shared cache.

Proposed defensive copy
     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, The setBody(byte[] body) method currently stores the caller's array directly
in the bytebody field which allows external mutations to alter the response;
change setBody to defensively copy the input (e.g., clone or Arrays.copyOf) into
this.bytebody and still clear this.body as before, and handle null inputs by
setting this.bytebody to null when body is null to avoid NPEs—refer to the
setBody(byte[]) method and the bytebody and body fields when making the change.

64-67: Javadoc uses /* instead of /** — won't be recognized as Javadoc.

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 comment above the method that builds the HTTP response in class
HttpResponseBuilder is a block comment (/* ... */) instead of a Javadoc comment,
so replace it with a proper Javadoc block (/** ... */) placed immediately above
the response-building method (e.g., buildResponse or build) and keep the
existing description and the `@return` tag so the method's documentation is
generated correctly.
🤖 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 14: The Dockerfile COPY instruction "COPY /www/ /www/" is copying from
the build context (and uses a non-idiomatic leading slash); change it to copy
from the build stage by using the named stage (e.g., "COPY --from=build www/
/www/") and remove the leading slash on the source, or point to the correct
stage name used in the Dockerfile if it's not "build"; ensure the referenced
build stage exists and that the source path "www/" is present in that stage.
- Around line 11-16: The Dockerfile copies compiled classes to / but the
ENTRYPOINT uses -classpath /app:/dependencies/* so org.example.App won't be
found; fix by ensuring classes are placed under /app or update the classpath:
change the COPY --from=build /build/target/classes/ / to COPY --from=build
/build/target/classes/ /app/ (so WORKDIR /app/ contains the classes) or
alternatively update ENTRYPOINT to use -classpath /:/dependencies/*; modify the
COPY step that currently uses --from=build and the ENTRYPOINT invocation to keep
class file location and classpath consistent with org.example.App.

In `@README.md`:
- Around line 329-335: Update the README section so it no longer calls the
filter chain a "Future Feature" — indicate that a Filter Chain already exists
and that this PR adds CacheFilter; change the heading and prose to mention the
existing Filter Chain and explicitly list CacheFilter as implemented, and keep
other bullets (Request/response filtering, Authentication, Logging, Compression)
as planned/optional future additions; reference the symbols "Filter Chain" and
"CacheFilter" in the updated text so readers know caching is present and other
filters remain forthcoming.
- Around line 48-49: Remove the duplicated "git clone" token and correct the
directory name after cloning: change the first command to a single "git clone
https://github.com/ithsjava25/project-webserver-juv25g.git" and change the
subsequent "cd" target to "project-webserver-juv25g" (or use the repository
basename returned by the clone) so the two README commands are consistent.

---

Nitpick comments:
In `@src/main/java/org/example/http/HttpResponseBuilder.java`:
- Around line 45-48: The setBody(byte[] body) method currently stores the
caller's array directly in the bytebody field which allows external mutations to
alter the response; change setBody to defensively copy the input (e.g., clone or
Arrays.copyOf) into this.bytebody and still clear this.body as before, and
handle null inputs by setting this.bytebody to null when body is null to avoid
NPEs—refer to the setBody(byte[]) method and the bytebody and body fields when
making the change.
- Around line 64-67: The comment above the method that builds the HTTP response
in class HttpResponseBuilder is a block comment (/* ... */) instead of a Javadoc
comment, so replace it with a proper Javadoc block (/** ... */) placed
immediately above the response-building method (e.g., buildResponse or build)
and keep the existing description and the `@return` tag so the method's
documentation is generated correctly.

Dockerfile Outdated
WORKDIR /app/
COPY --from=build /build/target/classes/ /
COPY --from=build /build/target/dependency/ /dependencies/
COPY /www/ /www/
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

COPY /www/ /www/ copies from the build context, not from the build stage.

This COPY has no --from=build, so it pulls from the Docker build context (the repo root). If www/ doesn't exist in the repo at build time, the build will fail. Also, the leading / on the source path is unconventional — Docker resolves it relative to the build context anyway, but COPY www/ /www/ is the idiomatic form.

Proposed fix
-COPY /www/ /www/
+COPY www/ /www/
📝 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
COPY /www/ /www/
COPY www/ /www/
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Dockerfile` at line 14, The Dockerfile COPY instruction "COPY /www/ /www/" is
copying from the build context (and uses a non-idiomatic leading slash); change
it to copy from the build stage by using the named stage (e.g., "COPY
--from=build www/ /www/") and remove the leading slash on the source, or point
to the correct stage name used in the Dockerfile if it's not "build"; ensure the
referenced build stage exists and that the source path "www/" is present in that
stage.

Comment on lines +48 to +49
git clone git clone https://github.com/ithsjava25/project-webserver-juv25g.git
cd project-webserver
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 and wrong directory name.

Line 48 has git clone repeated twice. Line 49 uses cd project-webserver but the repository name from the URL is project-webserver-juv25g.

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

In `@README.md` around lines 48 - 49, Remove the duplicated "git clone" token and
correct the directory name after cloning: change the first command to a single
"git clone https://github.com/ithsjava25/project-webserver-juv25g.git" and
change the subsequent "cd" target to "project-webserver-juv25g" (or use the
repository basename returned by the clone) so the two README commands are
consistent.

Comment on lines +329 to +335
### 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 misleading — CacheFilter is being added in this PR.

This section labels the filter chain as a future feature, but this PR introduces CacheFilter. Consider updating the description to reflect that a filter chain already exists (with caching) and that additional filters (auth, compression, etc.) are planned.

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

In `@README.md` around lines 329 - 335, Update the README section so it no longer
calls the filter chain a "Future Feature" — indicate that a Filter Chain already
exists and that this PR adds CacheFilter; change the heading and prose to
mention the existing Filter Chain and explicitly list CacheFilter as
implemented, and keep other bullets (Request/response filtering, Authentication,
Logging, Compression) as planned/optional future additions; reference the
symbols "Filter Chain" and "CacheFilter" in the updated text so readers know
caching is present and other filters remain forthcoming.

@donne41
Copy link

donne41 commented Feb 23, 2026

En bra början!
Men jag tänker att new Filecache ska flyttas ut från cacheFilter till tcpServer klassen så att cachen är sparad mellan anslutningar, och en övre gräns för cachen så inte minnet springer iväg.
Även concurrentHashmap för trådsäkerhet sen får vi se hur vi implementerar det med chainFilter.

…mproved CacheFilter with LRU eviction; updated StaticFileHandler to use shared cache; updated and added tests accordingly
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: 7

🧹 Nitpick comments (5)
src/test/java/org/example/StaticFileHandlerTest.java (2)

31-31: Unused field handler at class level.

handler is declared on line 31 but never assigned or used — every test method creates its own local handler. Remove the field to avoid confusion.

🧹 Proposed fix
 class StaticFileHandlerTest {
-
-    private StaticFileHandler handler;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/StaticFileHandlerTest.java` at line 31, Remove the
unused class-level field "handler" declared in StaticFileHandlerTest; it is
never assigned or referenced because each test creates its own local handler, so
delete the line "private StaticFileHandler handler;" from the test class
(StaticFileHandlerTest) to avoid confusion and keep tests clean, and run tests
to confirm no compile errors remain.

206-225: Parameterized sanitization test covers key URI variants well.

Covers query string, fragment, and leading slash. Consider adding a percent-encoded variant (e.g., %2Findex.html) to match the sanitization concern flagged in StaticFileHandler.

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

In `@src/test/java/org/example/StaticFileHandlerTest.java` around lines 206 - 225,
Add a percent-encoded URI variant to the parameterized test so the sanitization
behavior is exercised for encoded slashes: update the CsvSource in
StaticFileHandlerTest (the sanitized_uris_should_return_200 test) to include a
value like "%2Findex.html" (and/or "%2Findex.html?foo=bar") and keep using
StaticFileHandler.sendGetRequest to issue the request and assert the response
still contains "HTTP/1.1 200 OK"; this ensures StaticFileHandler's sanitization
logic is validated for percent-encoded inputs.
src/main/java/org/example/CacheFilter.java (2)

11-17: Consider extracting a FileCache interface for testability and future flexibility.

Both the PR reviewer (kappsegla) and prior learnings recommend making the cache an interface so StaticFileHandler depends on an abstraction rather than the concrete CacheFilter. This enables swapping implementations (e.g., Redis, Caffeine) and simplifies unit-testing with mocks/stubs.

Based on learnings: "FileCache should be refactored to be an interface to allow for different cache implementations (e.g., in-memory, Redis) and should be dependency-injected into StaticFileHandler."

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

In `@src/main/java/org/example/CacheFilter.java` around lines 11 - 17, Extract a
FileCache interface and make StaticFileHandler depend on it instead of the
concrete CacheFilter: define an interface (e.g., FileCache) exposing the cache
operations used by StaticFileHandler (get, put, remove, size/byte accounting if
needed), implement that interface with the existing CacheFilter class (rename or
adapt methods in CacheFilter to match the interface), and inject FileCache into
StaticFileHandler via constructor or setter so tests can substitute mocks/stubs
and you can later add other implementations (Redis/Caffeine) without changing
StaticFileHandler.

95-115: LRU eviction scans the entire map on every call — O(n) per eviction.

evictLeastRecentlyUsed streams all entries to find the minimum lastAccessTime. Inside the while (shouldEvict) loop this is O(n) per eviction, and multiple evictions may be needed in a single addToCache call, giving O(n²) worst case. For the current max of 100 entries this is tolerable, but a LinkedHashMap in access-order (or a min-heap / priority queue on access time) would make eviction O(1)/O(log n) and scale better if the limit is ever raised.

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

In `@src/main/java/org/example/CacheFilter.java` around lines 95 - 115,
evictLeastRecentlyUsed currently scans the entire cache map (cache) each time,
causing O(n) per eviction and O(n²) worst-case in addToCache; replace this with
an access-ordered LinkedHashMap or a priority queue to avoid full scans: convert
the backing cache to a LinkedHashMap with accessOrder=true and implement removal
by overriding removeEldestEntry or explicitly remove the first entry when
evicting, ensuring access updates (lastAccessTime/accessCount) occur on every
get/put so order stays correct; alternatively, maintain a PriorityQueue keyed by
lastAccessTime and update the queue on access, then pop to evict; keep
currentBytes adjustments (currentBytes.addAndGet) and System.out eviction
logging in evictLeastRecentlyUsed (or the new eviction hook) and ensure
thread-safety around cache, currentBytes and any queue modifications.
src/main/java/org/example/StaticFileHandler.java (1)

14-19: Cache is a static singleton — prevents dependency injection and complicates testing.

The static final SHARED_CACHE field prevents injecting a different cache implementation (e.g., for tests, Redis, Caffeine) and tightly couples StaticFileHandler to CacheFilter. Both the PR reviewer (kappsegla, donne41) and prior learnings recommend moving the cache instantiation to the server class and injecting it via the constructor.

♻️ Sketch: constructor-injected cache
 public class StaticFileHandler {
     private static final String DEFAULT_WEB_ROOT = "www";
-    private static final CacheFilter SHARED_CACHE = new CacheFilter();
+    private final CacheFilter cache;
     private final String webRoot;

-    public StaticFileHandler() {
-        this(DEFAULT_WEB_ROOT);
+    public StaticFileHandler(CacheFilter cache) {
+        this(DEFAULT_WEB_ROOT, cache);
     }

-    public StaticFileHandler(String webRoot) {
+    public StaticFileHandler(String webRoot, CacheFilter cache) {
         this.webRoot = webRoot;
+        this.cache = cache;
     }

Based on learnings: "FileCache should be refactored to be an interface … and should be dependency-injected into StaticFileHandler."

🤖 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 14 - 19, The
SHARED_CACHE static singleton ties StaticFileHandler to CacheFilter and blocks
DI/testing; refactor by introducing a FileCache interface (or use existing
CacheFilter to implement it), remove the static SHARED_CACHE field, add a
FileCache parameter to StaticFileHandler's constructor and store it as an
instance field, update all references inside StaticFileHandler to use this
instance cache, and modify the server/bootstrap code to instantiate the desired
cache implementation (e.g., new CacheFilter() or a test double/Redis/Caffeine
implementation) and pass it into the StaticFileHandler constructor so tests can
inject mocks.
🤖 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/CacheFilter.java`:
- Around line 134-137: clearCache() performs cache.clear() and
currentBytes.set(0) separately, allowing a concurrent addToCache to race and
leave the map and counter inconsistent; protect both operations with the same
lock used by addToCache by wrapping the body of clearCache() in
synchronized(this) so cache.clear() and currentBytes.set(0) are executed
atomically under the same monitor as addToCache.
- Around line 68-90: The addToCache/shouldEvict logic can loop forever when the
incoming data.length > MAX_CACHE_BYTES; update addToCache to guard against
caching oversized items by first checking if data.length > MAX_CACHE_BYTES and
returning early (skip caching) or, alternately, change the eviction loop to
break if evictLeastRecentlyUsed finds the cache empty; specifically, in
addToCache add a pre-check using MAX_CACHE_BYTES and data.length and skip
adding, or after each evictLeastRecentlyUsed call verify cache.isEmpty() and
break to avoid an infinite loop. Ensure you reference addToCache, shouldEvict,
evictLeastRecentlyUsed and MAX_CACHE_BYTES in your change.
- Around line 44-63: The getOrFetch method has a TOCTOU race allowing multiple
threads to fetch the same URI; change it to perform an atomic check-and-populate
so only one thread calls provider.fetch(uri). Replace the containsKey/get
pattern in getOrFetch with a ConcurrentHashMap.computeIfAbsent-based flow (or a
per-key lock/Future pattern) that creates or returns a CacheEntry atomically;
inside the compute function call provider.fetch(uri) and construct the
CacheEntry, then call entry.recordAccess() for the returned entry (and still use
addToCache behavior only if you need eviction logic), ensuring cache
(ConcurrentHashMap), getOrFetch, addToCache, CacheEntry, and provider.fetch are
the touchpoints to update.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 59-69: sanitizeUri currently never URL-decodes the incoming URI,
so percent-encoded traversal sequences like "%2e%2e" bypass expectations; update
sanitizeUri to first URL-decode the raw URI (e.g., using URLDecoder.decode(uri,
"UTF-8") or equivalent) inside a try/catch to handle malformed-encoding, then
strip query/fragment, null bytes, and leading slashes as before; ensure errors
fall back to a safe rejection (e.g., return null or throw a handled exception)
so downstream checks in isPathTraversal and normalize operate on the decoded
path.
- Around line 39-42: The shared static cache key currently uses only
sanitizedUri which allows different StaticFileHandler instances with different
webRoot values to collide; update the cache key to include a stable identifier
derived from webRoot (e.g., prefix the key with webRoot or its absolute
path/URI) when calling SHARED_CACHE.getOrFetch, and ensure the FileProvider
lambda still resolves the file using the original sanitizedUri and the handler's
webRoot (so strip the prefix or capture sanitizedUri separately). Reference
SHARED_CACHE, sanitizedUri, webRoot, and the StaticFileHandler code path that
calls SHARED_CACHE.getOrFetch; alternatively scope the cache per webRoot
instance if you prefer not to change keys.
- Around line 44-46: StaticFileHandler currently hardcodes the Content-Type
header to "text/html; charset=UTF-8" when building responses; instead call the
existing helper to derive the correct MIME type for the served file (use
MimeTypeDetector or HttpResponseBuilder.setContentTypeFromFilename) and set that
on the HttpResponseBuilder before setBody(fileBytes), ensuring text/* types
include charset=UTF-8 as appropriate.

In `@src/test/java/org/example/StaticFileHandlerTest.java`:
- Around line 104-137: The test spawns threads where assertion failures
(AssertionError) are not caught by the catch(IOException) and thus are lost, so
update testConcurrent_MultipleReads to propagate thread failures to the main
test: replace raw Thread usage with an ExecutorService submitting
Callables/Runnables and collect Futures (or use a shared
AtomicReference<Throwable>) and after starting all tasks call future.get() (or
check AtomicReference and rethrow if non-null) so any AssertionError from
handler.sendGetRequest/assertThat is rethrown in the main thread; reference
testConcurrent_MultipleReads, handler.sendGetRequest, and
StaticFileHandler.getCacheStats() when making the change.

---

Nitpick comments:
In `@src/main/java/org/example/CacheFilter.java`:
- Around line 11-17: Extract a FileCache interface and make StaticFileHandler
depend on it instead of the concrete CacheFilter: define an interface (e.g.,
FileCache) exposing the cache operations used by StaticFileHandler (get, put,
remove, size/byte accounting if needed), implement that interface with the
existing CacheFilter class (rename or adapt methods in CacheFilter to match the
interface), and inject FileCache into StaticFileHandler via constructor or
setter so tests can substitute mocks/stubs and you can later add other
implementations (Redis/Caffeine) without changing StaticFileHandler.
- Around line 95-115: evictLeastRecentlyUsed currently scans the entire cache
map (cache) each time, causing O(n) per eviction and O(n²) worst-case in
addToCache; replace this with an access-ordered LinkedHashMap or a priority
queue to avoid full scans: convert the backing cache to a LinkedHashMap with
accessOrder=true and implement removal by overriding removeEldestEntry or
explicitly remove the first entry when evicting, ensuring access updates
(lastAccessTime/accessCount) occur on every get/put so order stays correct;
alternatively, maintain a PriorityQueue keyed by lastAccessTime and update the
queue on access, then pop to evict; keep currentBytes adjustments
(currentBytes.addAndGet) and System.out eviction logging in
evictLeastRecentlyUsed (or the new eviction hook) and ensure thread-safety
around cache, currentBytes and any queue modifications.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 14-19: The SHARED_CACHE static singleton ties StaticFileHandler to
CacheFilter and blocks DI/testing; refactor by introducing a FileCache interface
(or use existing CacheFilter to implement it), remove the static SHARED_CACHE
field, add a FileCache parameter to StaticFileHandler's constructor and store it
as an instance field, update all references inside StaticFileHandler to use this
instance cache, and modify the server/bootstrap code to instantiate the desired
cache implementation (e.g., new CacheFilter() or a test double/Redis/Caffeine
implementation) and pass it into the StaticFileHandler constructor so tests can
inject mocks.

In `@src/test/java/org/example/StaticFileHandlerTest.java`:
- Line 31: Remove the unused class-level field "handler" declared in
StaticFileHandlerTest; it is never assigned or referenced because each test
creates its own local handler, so delete the line "private StaticFileHandler
handler;" from the test class (StaticFileHandlerTest) to avoid confusion and
keep tests clean, and run tests to confirm no compile errors remain.
- Around line 206-225: Add a percent-encoded URI variant to the parameterized
test so the sanitization behavior is exercised for encoded slashes: update the
CsvSource in StaticFileHandlerTest (the sanitized_uris_should_return_200 test)
to include a value like "%2Findex.html" (and/or "%2Findex.html?foo=bar") and
keep using StaticFileHandler.sendGetRequest to issue the request and assert the
response still contains "HTTP/1.1 200 OK"; this ensures StaticFileHandler's
sanitization logic is validated for percent-encoded inputs.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 515bc8c and 0a3c08c.

📒 Files selected for processing (3)
  • src/main/java/org/example/CacheFilter.java
  • src/main/java/org/example/StaticFileHandler.java
  • src/test/java/org/example/StaticFileHandlerTest.java

Copy link

@donne41 donne41 left a comment

Choose a reason for hiding this comment

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

Jag känner mig lite sträng men tycker det är en del som är ändrat som inte behöver ha ändrats. Förutom det så tror jag vi börjar närma oss!

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.

♻️ Duplicate comments (3)
src/main/java/org/example/CacheFilter.java (3)

118-121: ⚠️ Potential issue | 🟡 Minor

Make clearCache() atomic to avoid counter drift.

cache.clear() and currentBytes.set(0) can race with addToCache, leaving the map and byte counter inconsistent. Synchronize the method or wrap both operations in a synchronized block.

🛡️ Suggested fix
-public void clearCache() {
+public synchronized void clearCache() {
     cache.clear();
     currentBytes.set(0);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/CacheFilter.java` around lines 118 - 121, The
clearCache method can race with addToCache causing the cache map and
currentBytes counter to drift; make clearCache atomic by synchronizing it (or
wrapping its body in a synchronized block) using the same lock used by
addToCache (e.g., a dedicated lock object or synchronized(this)) so that both
cache.clear() and currentBytes.set(0) execute under the same lock as
addToCache's updates; update clearCache to acquire that lock before clearing to
ensure consistency between cache and currentBytes.

68-72: ⚠️ Potential issue | 🔴 Critical

Guard against infinite eviction when a single entry exceeds the cache budget.

If data.length > MAX_CACHE_BYTES, shouldEvict remains true forever and evictLeastRecentlyUsed() can’t make room once the cache is empty. Skip caching oversized entries (or break when empty).

🐛 Suggested fix (oversize guard + empty check)
 private synchronized void addToCache(String uri, byte[] data) {
+    if (data.length > MAX_CACHE_BYTES) {
+        return; // too large to ever fit
+    }
     // Kontrollera om vi måste evicta innan vi lägger till
     while (shouldEvict(data)) {
+        if (cache.isEmpty()) {
+            return;
+        }
         evictLeastRecentlyUsed();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/CacheFilter.java` around lines 68 - 72, The
addToCache method can loop forever if the incoming data is larger than
MAX_CACHE_BYTES because shouldEvict stays true even when the cache is empty;
update addToCache (and its eviction loop) to first skip caching when data.length
> MAX_CACHE_BYTES, and additionally break the eviction loop if the cache is
already empty (e.g., after evictLeastRecentlyUsed returns no-op or you detect
currentCacheBytes == 0), so that addToCache either abandons oversized entries or
exits the loop safely before attempting endless evictions.

44-63: ⚠️ Potential issue | 🟠 Major

Avoid TOCTOU on cache miss to prevent duplicate fetches.

containsKey + get is not atomic, so concurrent misses can trigger multiple provider.fetch(uri) calls for the same resource. Consider a double-checked synchronized block (or a computeIfAbsent approach with exception handling) so only one thread fetches and populates.

🔧 Suggested fix (double-check under lock)
 public byte[] getOrFetch(String uri, FileProvider provider) throws IOException {
-    // Kontrollera cache (lock-free read)
-    if (cache.containsKey(uri)) {
-        CacheEntry entry = cache.get(uri);
-        if (entry != null) {
-            entry.recordAccess();
-            System.out.println(" Cache hit for: " + uri);
-            return entry.data;
-        }
-    }
+    CacheEntry entry = cache.get(uri);
+    if (entry != null) {
+        entry.recordAccess();
+        System.out.println(" Cache hit for: " + uri);
+        return entry.data;
+    }

     // Cache miss - fetch från provider
     System.out.println(" Cache miss for: " + uri);
-    byte[] fileBytes = provider.fetch(uri);
-    
-    // Lägg till i cache
-    addToCache(uri, fileBytes);
-    
-    return fileBytes;
+    synchronized (this) {
+        entry = cache.get(uri);
+        if (entry != null) {
+            entry.recordAccess();
+            return entry.data;
+        }
+        byte[] fileBytes = provider.fetch(uri);
+        addToCache(uri, fileBytes);
+        return fileBytes;
+    }
 }

Based on learnings: Ensure thread-safe FileCache implementations across the codebase. Do not use plain HashMap for shared caches. Use ConcurrentHashMap or proper synchronization to avoid data races when accessed by multiple request threads. Prefer thread-safe APIs (e.g., computeIfAbsent) and guard compound actions with locking if using custom synchronization.

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

In `@src/main/java/org/example/CacheFilter.java` around lines 44 - 63, The
getOrFetch method has a TOCTOU race because it uses cache.containsKey +
cache.get causing duplicate provider.fetch(uri) under concurrent misses; change
the cache to a ConcurrentHashMap (if not already) and replace the contains/get
pattern with an atomic computeIfAbsent or a double-checked synchronized block
around the cache population so only one thread invokes provider.fetch(uri) for a
given uri; ensure you reference the CacheEntry creation path used by
addToCache/CacheEntry so computeIfAbsent returns a fully-populated CacheEntry
(or have computeIfAbsent store the fetched byte[] wrapped in a new CacheEntry)
and make sure to handle and propagate provider.fetch exceptions without
inserting null entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/main/java/org/example/CacheFilter.java`:
- Around line 118-121: The clearCache method can race with addToCache causing
the cache map and currentBytes counter to drift; make clearCache atomic by
synchronizing it (or wrapping its body in a synchronized block) using the same
lock used by addToCache (e.g., a dedicated lock object or synchronized(this)) so
that both cache.clear() and currentBytes.set(0) execute under the same lock as
addToCache's updates; update clearCache to acquire that lock before clearing to
ensure consistency between cache and currentBytes.
- Around line 68-72: The addToCache method can loop forever if the incoming data
is larger than MAX_CACHE_BYTES because shouldEvict stays true even when the
cache is empty; update addToCache (and its eviction loop) to first skip caching
when data.length > MAX_CACHE_BYTES, and additionally break the eviction loop if
the cache is already empty (e.g., after evictLeastRecentlyUsed returns no-op or
you detect currentCacheBytes == 0), so that addToCache either abandons oversized
entries or exits the loop safely before attempting endless evictions.
- Around line 44-63: The getOrFetch method has a TOCTOU race because it uses
cache.containsKey + cache.get causing duplicate provider.fetch(uri) under
concurrent misses; change the cache to a ConcurrentHashMap (if not already) and
replace the contains/get pattern with an atomic computeIfAbsent or a
double-checked synchronized block around the cache population so only one thread
invokes provider.fetch(uri) for a given uri; ensure you reference the CacheEntry
creation path used by addToCache/CacheEntry so computeIfAbsent returns a
fully-populated CacheEntry (or have computeIfAbsent store the fetched byte[]
wrapped in a new CacheEntry) and make sure to handle and propagate
provider.fetch exceptions without inserting null entries.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a3c08c and d20bb6f.

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

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/test/java/org/example/StaticFileHandlerTest.java (1)

152-154: ⚠️ Potential issue | 🟡 Minor

Dead test setup: pageNotFound.html is never read by the handler.

StaticFileHandler.sendGetRequest calls sendErrorResponse(outputStream, 404, "Not Found") directly on IOException — it does not look up any pageNotFound.html file from disk. The Files.writeString(testFile, "Fallback page") on line 154 has no effect on the test outcome and can be removed to keep the test's intent clear.

🧹 Proposed cleanup
-        // Pre-create the mandatory error page in the temp directory to prevent NoSuchFileException
-        Path testFile = tempDir.resolve("pageNotFound.html");
-        Files.writeString(testFile, "Fallback page");
-
-        //Using the new constructor in StaticFileHandler to reroute so the tests uses the temporary folder instead of the hardcoded www
         StaticFileHandler staticFileHandler = new StaticFileHandler(tempDir.toString());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/StaticFileHandlerTest.java` around lines 152 - 154,
The test creates a temp file "pageNotFound.html" but
StaticFileHandler.sendGetRequest invokes sendErrorResponse(outputStream, 404,
"Not Found") on IOException and never reads that file; remove the dead setup by
deleting the Files.writeString(testFile, "Fallback page") call (leave the Path
testFile = tempDir.resolve("pageNotFound.html") only if needed for other
assertions or remove the Path creation too), so the test reflects the handler
behavior in StaticFileHandler.sendGetRequest and no longer writes an unused
fallback file.
♻️ Duplicate comments (3)
src/test/java/org/example/StaticFileHandlerTest.java (1)

89-122: Assertions inside threads are silently swallowed — test can falsely pass.

AssertionError from assertThat(out.toString()).contains("200") is not a subtype of IOException, so the catch (IOException e) block on line 108 does not catch it. It escapes as an unchecked exception in the child thread; thread.join() does not rethrow it. Every inner assertion can fail while the test reports green.

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

In `@src/test/java/org/example/StaticFileHandlerTest.java` around lines 89 - 122,
The assertion inside the spawned threads in testConcurrent_MultipleReads can be
lost because AssertionError thrown in child threads won’t propagate to the test
thread; update the thread bodies to catch Throwable (not just IOException) and
record any failure into a shared thread-safe container (e.g.,
AtomicReference<Throwable> or a ConcurrentLinkedQueue<Throwable>), then after
joining all threads check the container and rethrow the first Throwable (or fail
the test) so failures from the invoked handler.sendGetRequest/out assertions
surface; alternatively replace raw Threads with an ExecutorService and submit
Callables or Futures and call get() to propagate exceptions from
sendGetRequest/assertions back to the main test thread.
src/main/java/org/example/StaticFileHandler.java (2)

39-41: Cache key does not incorporate webRoot — different handler instances will return stale/wrong file content.

The cache key is only sanitizedUri (e.g., "index.html"). Two StaticFileHandler instances with different webRoot paths that both serve "index.html" will share the same cache slot; whichever populates it first wins, and subsequent requests from the other root silently receive the wrong bytes.

🤖 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 39 - 41, The
cache key currently uses only sanitizedUri in SHARED_CACHE.getOrFetch (inside
StaticFileHandler) which causes collisions across handlers with different
webRoot; change the key to incorporate webRoot (e.g., combine webRoot's
canonical/absolute path or realPath with sanitizedUri) so each (webRoot,
sanitizedUri) pair is unique, and continue passing the sanitizedUri to the
loader (Files.readAllBytes(new File(webRoot, path).toPath())); ensure you
normalize/resolve webRoot to avoid equivalent-path mismatches before composing
the key.

54-68: sanitizeUri skips URL-decoding — percent-encoded traversal sequences aren't normalized.

A URI like %2e%2e/%2e%2e/etc/passwd passes through unmodified; normalize() in isPathTraversal never sees .. and cannot collapse the segments. Today this fails safely as a 404 (literal %2e%2e directory doesn't exist), but the safe failure is incidental, not deliberate.

🤖 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 54 - 68, The
sanitizeUri method currently slices off query/fragment but never percent-decodes
the path, so percent-encoded traversal like "%2e%2e" bypasses normalization;
update sanitizeUri to URL-decode the path segment (after removing query/fragment
but before the null/leading-slash cleanup) using UTF-8 (e.g.,
java.net.URLDecoder.decode(..., StandardCharsets.UTF_8)) and handle
malformed-encoding exceptions by treating the URI as invalid (return null or
equivalent) so that isPathTraversal's normalize/collapse logic sees actual ".."
segments and can detect traversal correctly.
🧹 Nitpick comments (2)
src/main/java/org/example/CacheFilter.java (2)

53-53: Replace System.out.println diagnostics with a proper logger.

Scattered System.out.println calls throughout hit/miss/eviction paths are not configurable and cannot be suppressed in production or redirected to a log aggregator. Replace with java.util.logging.Logger (or SLF4J if already on the classpath) at FINE/DEBUG level.

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

In `@src/main/java/org/example/CacheFilter.java` at line 53, Replace the
System.out.println diagnostics in CacheFilter (the print in the cache
hit/miss/eviction paths) with a proper logger: add a class-level
java.util.logging.Logger (or org.slf4j.Logger if available) instance in
CacheFilter, remove the System.out.println call, and emit the message via
logger.fine(...) (or logger.debug(...) for SLF4J) using the same URI context
(e.g., "Cache hit for: " + uri) so messages are configurable by logging level
and can be redirected in production.

13-13: CacheFilter should be an interface to support swappable cache implementations.

The class is named CacheFilter but is a concrete in-memory implementation. StaticFileHandler hardwires new CacheFilter() as a static final field, making it impossible to substitute a different cache backend (e.g., Redis, Caffeine, test-double) without changing StaticFileHandler itself.

Extract a FileCache interface exposing getOrFetch, clearCache, and getStats, rename this class to something like InMemoryFileCache, and inject the interface into StaticFileHandler via its constructor.

Based on learnings from kappsegla: FileCache should be refactored to be an interface to allow for different cache implementations (e.g., in-memory, Redis) and should be dependency-injected into StaticFileHandler.

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

In `@src/main/java/org/example/CacheFilter.java` at line 13, The current concrete
class CacheFilter should be extracted into a FileCache interface declaring
getOrFetch(...), clearCache(), and getStats(), then rename the existing class to
InMemoryFileCache implementing FileCache; update StaticFileHandler to stop using
the static final new CacheFilter() and instead accept a FileCache in its
constructor (store as a field) so callers can inject different implementations
(e.g., RedisFileCache or test doubles), and update all call sites to use the new
interface type and methods.
🤖 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/StaticFileHandler.java`:
- Around line 69-78: The isPathTraversal method currently resolves symlinks only
for webRoot but not for the requested path, allowing symlink bypass; update
isPathTraversal so that after resolving webRoot via
Paths.get(webRoot).toRealPath() you also call toRealPath() on the resolved
requestedPath (instead of only normalize()) before checking
startsWith(webRootPath); keep the existing catch (IOException e) → return true
behavior to preserve the handling when the target does not exist.

---

Outside diff comments:
In `@src/test/java/org/example/StaticFileHandlerTest.java`:
- Around line 152-154: The test creates a temp file "pageNotFound.html" but
StaticFileHandler.sendGetRequest invokes sendErrorResponse(outputStream, 404,
"Not Found") on IOException and never reads that file; remove the dead setup by
deleting the Files.writeString(testFile, "Fallback page") call (leave the Path
testFile = tempDir.resolve("pageNotFound.html") only if needed for other
assertions or remove the Path creation too), so the test reflects the handler
behavior in StaticFileHandler.sendGetRequest and no longer writes an unused
fallback file.

---

Duplicate comments:
In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 39-41: The cache key currently uses only sanitizedUri in
SHARED_CACHE.getOrFetch (inside StaticFileHandler) which causes collisions
across handlers with different webRoot; change the key to incorporate webRoot
(e.g., combine webRoot's canonical/absolute path or realPath with sanitizedUri)
so each (webRoot, sanitizedUri) pair is unique, and continue passing the
sanitizedUri to the loader (Files.readAllBytes(new File(webRoot,
path).toPath())); ensure you normalize/resolve webRoot to avoid equivalent-path
mismatches before composing the key.
- Around line 54-68: The sanitizeUri method currently slices off query/fragment
but never percent-decodes the path, so percent-encoded traversal like "%2e%2e"
bypasses normalization; update sanitizeUri to URL-decode the path segment (after
removing query/fragment but before the null/leading-slash cleanup) using UTF-8
(e.g., java.net.URLDecoder.decode(..., StandardCharsets.UTF_8)) and handle
malformed-encoding exceptions by treating the URI as invalid (return null or
equivalent) so that isPathTraversal's normalize/collapse logic sees actual ".."
segments and can detect traversal correctly.

In `@src/test/java/org/example/StaticFileHandlerTest.java`:
- Around line 89-122: The assertion inside the spawned threads in
testConcurrent_MultipleReads can be lost because AssertionError thrown in child
threads won’t propagate to the test thread; update the thread bodies to catch
Throwable (not just IOException) and record any failure into a shared
thread-safe container (e.g., AtomicReference<Throwable> or a
ConcurrentLinkedQueue<Throwable>), then after joining all threads check the
container and rethrow the first Throwable (or fail the test) so failures from
the invoked handler.sendGetRequest/out assertions surface; alternatively replace
raw Threads with an ExecutorService and submit Callables or Futures and call
get() to propagate exceptions from sendGetRequest/assertions back to the main
test thread.

---

Nitpick comments:
In `@src/main/java/org/example/CacheFilter.java`:
- Line 53: Replace the System.out.println diagnostics in CacheFilter (the print
in the cache hit/miss/eviction paths) with a proper logger: add a class-level
java.util.logging.Logger (or org.slf4j.Logger if available) instance in
CacheFilter, remove the System.out.println call, and emit the message via
logger.fine(...) (or logger.debug(...) for SLF4J) using the same URI context
(e.g., "Cache hit for: " + uri) so messages are configurable by logging level
and can be redirected in production.
- Line 13: The current concrete class CacheFilter should be extracted into a
FileCache interface declaring getOrFetch(...), clearCache(), and getStats(),
then rename the existing class to InMemoryFileCache implementing FileCache;
update StaticFileHandler to stop using the static final new CacheFilter() and
instead accept a FileCache in its constructor (store as a field) so callers can
inject different implementations (e.g., RedisFileCache or test doubles), and
update all call sites to use the new interface type and methods.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d20bb6f and c158677.

📒 Files selected for processing (3)
  • src/main/java/org/example/CacheFilter.java
  • src/main/java/org/example/StaticFileHandler.java
  • src/test/java/org/example/StaticFileHandlerTest.java

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: 4

♻️ Duplicate comments (1)
src/main/java/org/example/StaticFileHandler.java (1)

105-119: ⚠️ Potential issue | 🟠 Major

Symlink traversal still bypasses the path-jail check.

The comment on lines 107–108 states that toAbsolutePath().normalize() handles symlinks safely, but it does the opposite — normalize() only collapses textual .. segments and does not resolve symlinks. A symlink inside webRoot pointing to /etc/passwd will pass startsWith(webRootPath) and then Files.readAllBytes on line 48 will follow the symlink and serve the target file.

The previous review already flagged this and suggested using toRealPath() on the resolved requested path. The current code explicitly chose not to, citing "to avoid requirements on directory existence," but the try/catch on line 114 already handles the IOException that toRealPath() throws for non-existent files.

🛡️ Proposed fix
     private boolean isPathTraversal(String uri) {
         try {
-            Path webRootPath = Paths.get(webRoot).toAbsolutePath().normalize();
-            Path requestedPath = webRootPath.resolve(uri).normalize();
-
-            return !requestedPath.startsWith(webRootPath);
+            Path webRootPath = Paths.get(webRoot).toRealPath();
+            Path requestedPath = webRootPath.resolve(uri).toRealPath();
+            return !requestedPath.startsWith(webRootPath);
         } catch (Exception e) {
+            // toRealPath() throws IOException for non-existent files — safe default
             return true;
         }
     }
🤖 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 105 - 119, The
isPathTraversal method is still vulnerable to symlink bypass because normalize()
doesn't resolve symlinks; change the requestedPath resolution to resolve
symlinks by calling toRealPath(...) on the resolved path (e.g., replace
requestedPath = webRootPath.resolve(uri).normalize() with requestedPath =
webRootPath.resolve(uri).toRealPath() or similar) before performing the
startsWith(webRootPath) check so any symlink targets are resolved and caught;
keep the existing try/catch (it already handles the IOException from toRealPath)
and ensure the final comparison still uses the canonical/real webRootPath if
needed.
🧹 Nitpick comments (3)
src/main/java/org/example/CacheFilter.java (2)

36-40: Cached byte arrays are returned by reference — callers can silently corrupt the cache.

CacheEntry stores and exposes the raw data reference. If any caller mutates the returned byte[], the cached content is corrupted for all subsequent requests. Current callers (StaticFileHandler) don't modify the array, so this isn't an active bug, but it's fragile.

🛡️ Defensive copy on read
     // In getOrFetch, at cache-hit return sites:
-            return entry.data;
+            return entry.data.clone();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/CacheFilter.java` around lines 36 - 40, CacheEntry
currently stores and exposes the raw byte[] which allows callers to mutate
cached content; update CacheEntry to defensively copy data on input and/or on
output: in the CacheEntry(byte[] data) constructor copy the array into a new
byte[] before assigning to the data field, and ensure any accessor (e.g.,
getData or usages that return entry.data) returns a copy rather than the
internal reference so callers cannot mutate the cache.

52-84: All cache misses serialize on writeLock, including disk I/O.

provider.fetch(uri) (disk read) executes while holding writeLock (line 76). This means two concurrent requests for different uncached URIs block each other. Under a burst of cold-cache requests this becomes a bottleneck.

Consider a per-key locking or Future-based pattern so only requests for the same key wait on each other, while different keys can fetch concurrently.

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

In `@src/main/java/org/example/CacheFilter.java` around lines 52 - 84, The method
getOrFetch currently performs provider.fetch(uri) while holding the shared
writeLock, causing unrelated cache misses to serialize; change to a per-key
in-flight/future pattern so only requests for the same uri block each other:
keep the initial lock-free cache.get(uri) check and early return, then use a
ConcurrentHashMap<String, CompletableFuture<byte[]>> (e.g., inFlightFetches) and
call computeIfAbsent(uri, key -> CompletableFuture.supplyAsync(() ->
provider.fetch(key))) to start the fetch outside the global writeLock; when the
future completes, if bytes != null call addToCacheUnsafe(uri, bytes) under the
writeLock (or via a short synchronized block) and remove the future from
inFlightFetches; finally return the future.join() (or get) result and ensure
recordAccess() is still called for cache hits (including after concurrent fetch
completes).
src/main/java/org/example/StaticFileHandler.java (1)

18-34: Shared cache is hardcoded rather than injected — consider constructor injection.

SHARED_CACHE is static final new CacheFilter(), which couples StaticFileHandler to the concrete implementation. The FileCache interface was introduced specifically to enable pluggable backends. Accepting a FileCache via constructor (or a static setter for the shared instance) would complete the decoupling and simplify testing.

🤖 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 18 - 34,
SHARED_CACHE is tightly coupled to CacheFilter; change StaticFileHandler to
accept a FileCache instead of hardcoding new CacheFilter: add a constructor
overload StaticFileHandler(FileCache cache, String webRoot) (and a convenience
StaticFileHandler(FileCache cache) or static setter if a shared instance is
required), store the injected cache in a non-static or static field (replace
SHARED_CACHE usage), and update any code paths that reference SHARED_CACHE to
use the injected FileCache so tests can pass mock implementations and different
backends can be provided.
🤖 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/FileCache.java`:
- Around line 57-64: The toString() in CacheStats can produce Infinity/NaN when
maxBytes is zero; update CacheStats.toString() to guard the division by zero
(use a conditional like maxBytes > 0 ? (double) bytes / maxBytes * 100 : 0.0)
when computing utilization so the formatted string shows a sane percentage
(e.g., 0.0%) instead of Infinity/NaN; keep the same String.format call and
replace the utilization expression only, referencing entries, maxEntries, bytes,
maxBytes, and totalAccesses in CacheStats.toString().

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 76-98: In sanitizeUri change the query/fragment index checks so a
delimiter at position 0 is recognized: compute queryIndex and fragmentIndex
using uri.indexOf('?') / uri.indexOf('#') and treat values >= 0 (or use != -1)
instead of using > 0 when building endIndex; update the endIndex calculation
that chooses the minimum of the two adjusted bounds so leading '?' or '#' are
stripped from the returned path (function: sanitizeUri).
- Around line 100-101: The duplicate Javadoc opener in the StaticFileHandler
class leads to malformed documentation; remove the extra `/**` so only a single
Javadoc block surrounds the intended comment (or if the intent was two separate
docs, ensure each has its own opening `/**` and matching `*/`). Locate the
duplicate `/**` near the top of class StaticFileHandler and either delete the
stray opener or add the missing closing `*/` to properly terminate the first
block, ensuring Javadoc syntax is correct for the affected comment block.

In `@src/test/java/org/example/StaticFileHandlerTest.java`:
- Around line 82-128: The testConcurrent_MultipleReads test currently only
records AssertionError from child threads and wraps IOExceptions in
RuntimeException which can terminate threads silently; update the thread run()
catch logic in testConcurrent_MultipleReads so it captures any Throwable
(including IOException/RuntimeException) into the shared assertionErrors slot
(or a new Throwable[] errors) while synchronizing on threads before rethrowing,
so the main thread can detect and rethrow the first child exception after join;
reference handler.sendGetRequest(...) and the threads[]/assertionErrors[] usage
when making this change.

---

Duplicate comments:
In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 105-119: The isPathTraversal method is still vulnerable to symlink
bypass because normalize() doesn't resolve symlinks; change the requestedPath
resolution to resolve symlinks by calling toRealPath(...) on the resolved path
(e.g., replace requestedPath = webRootPath.resolve(uri).normalize() with
requestedPath = webRootPath.resolve(uri).toRealPath() or similar) before
performing the startsWith(webRootPath) check so any symlink targets are resolved
and caught; keep the existing try/catch (it already handles the IOException from
toRealPath) and ensure the final comparison still uses the canonical/real
webRootPath if needed.

---

Nitpick comments:
In `@src/main/java/org/example/CacheFilter.java`:
- Around line 36-40: CacheEntry currently stores and exposes the raw byte[]
which allows callers to mutate cached content; update CacheEntry to defensively
copy data on input and/or on output: in the CacheEntry(byte[] data) constructor
copy the array into a new byte[] before assigning to the data field, and ensure
any accessor (e.g., getData or usages that return entry.data) returns a copy
rather than the internal reference so callers cannot mutate the cache.
- Around line 52-84: The method getOrFetch currently performs
provider.fetch(uri) while holding the shared writeLock, causing unrelated cache
misses to serialize; change to a per-key in-flight/future pattern so only
requests for the same uri block each other: keep the initial lock-free
cache.get(uri) check and early return, then use a ConcurrentHashMap<String,
CompletableFuture<byte[]>> (e.g., inFlightFetches) and call computeIfAbsent(uri,
key -> CompletableFuture.supplyAsync(() -> provider.fetch(key))) to start the
fetch outside the global writeLock; when the future completes, if bytes != null
call addToCacheUnsafe(uri, bytes) under the writeLock (or via a short
synchronized block) and remove the future from inFlightFetches; finally return
the future.join() (or get) result and ensure recordAccess() is still called for
cache hits (including after concurrent fetch completes).

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 18-34: SHARED_CACHE is tightly coupled to CacheFilter; change
StaticFileHandler to accept a FileCache instead of hardcoding new CacheFilter:
add a constructor overload StaticFileHandler(FileCache cache, String webRoot)
(and a convenience StaticFileHandler(FileCache cache) or static setter if a
shared instance is required), store the injected cache in a non-static or static
field (replace SHARED_CACHE usage), and update any code paths that reference
SHARED_CACHE to use the injected FileCache so tests can pass mock
implementations and different backends can be provided.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c158677 and f5b2f44.

📒 Files selected for processing (4)
  • src/main/java/org/example/CacheFilter.java
  • src/main/java/org/example/FileCache.java
  • src/main/java/org/example/StaticFileHandler.java
  • src/test/java/org/example/StaticFileHandlerTest.java

Comment on lines +57 to +64
@Override
public String toString() {
return String.format(
"CacheStats{entries=%d/%d, bytes=%d/%d, utilization=%.1f%%, accesses=%d}",
entries, maxEntries, bytes, maxBytes,
(double) bytes / maxBytes * 100, totalAccesses
);
}
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

toString() produces Infinity% when maxBytes is zero.

If a CacheStats is ever constructed with maxBytes == 0, the expression (double) bytes / maxBytes * 100 yields Infinity or NaN, producing an unreadable string.

🛡️ Proposed fix
         return String.format(
                 "CacheStats{entries=%d/%d, bytes=%d/%d, utilization=%.1f%%, accesses=%d}",
                 entries, maxEntries, bytes, maxBytes,
-                (double) bytes / maxBytes * 100, totalAccesses
+                maxBytes > 0 ? (double) bytes / maxBytes * 100 : 0.0, totalAccesses
         );
📝 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
@Override
public String toString() {
return String.format(
"CacheStats{entries=%d/%d, bytes=%d/%d, utilization=%.1f%%, accesses=%d}",
entries, maxEntries, bytes, maxBytes,
(double) bytes / maxBytes * 100, totalAccesses
);
}
`@Override`
public String toString() {
return String.format(
"CacheStats{entries=%d/%d, bytes=%d/%d, utilization=%.1f%%, accesses=%d}",
entries, maxEntries, bytes, maxBytes,
maxBytes > 0 ? (double) bytes / maxBytes * 100 : 0.0, totalAccesses
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/FileCache.java` around lines 57 - 64, The
toString() in CacheStats can produce Infinity/NaN when maxBytes is zero; update
CacheStats.toString() to guard the division by zero (use a conditional like
maxBytes > 0 ? (double) bytes / maxBytes * 100 : 0.0) when computing utilization
so the formatted string shows a sane percentage (e.g., 0.0%) instead of
Infinity/NaN; keep the same String.format call and replace the utilization
expression only, referencing entries, maxEntries, bytes, maxBytes, and
totalAccesses in CacheStats.toString().

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

♻️ Duplicate comments (4)
src/test/java/org/example/StaticFileHandlerTest.java (1)

93-94: ⚠️ Potential issue | 🟡 Minor

Child-thread AssertionError is not captured.

Line 108 catches Exception, but Line 105 throws AssertionError (Error), so thread assertions can be lost.

💚 Proposed fix
-        final Exception[] threadError = new Exception[1];
+        final Throwable[] threadError = new Throwable[1];
...
-                } catch (Exception e) {
+                } catch (Throwable e) {
                     synchronized (threads) {
-                        threadError[0] = e;
+                        if (threadError[0] == null) {
+                            threadError[0] = e;
+                        }
                     }
                 }
...
         if (threadError[0] != null) {
             throw new AssertionError("Tråd-fel: " + threadError[0].getMessage(), threadError[0]);
         }

Also applies to: 105-110, 123-125

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

In `@src/test/java/org/example/StaticFileHandlerTest.java` around lines 93 - 94,
The test spawns a child thread but only captures Exception in final Exception[]
threadError, so AssertionError (which extends Error/Throwable) can be lost;
update the capture to use Throwable (e.g., final Throwable[] threadError or an
AtomicReference<Throwable>) and change the thread's catch block to catch
Throwable instead of Exception, then after joining rethrow or fail with the
captured Throwable (preserving AssertionError) so assertions in the child thread
are surfaced; reference the threadError variable and the thread's run/catch
block where the error is stored and checked.
src/main/java/org/example/StaticFileHandler.java (2)

85-87: ⚠️ Potential issue | 🟡 Minor

Fix off-by-one in query/fragment stripping.

Line 85 and Line 86 should treat index 0 as a found delimiter; otherwise URIs like "?x=1" are not sanitized correctly.

🐛 Proposed fix
         int endIndex = Math.min(
-                queryIndex > 0 ? queryIndex : uri.length(),
-                fragmentIndex > 0 ? fragmentIndex : uri.length()
+                queryIndex >= 0 ? queryIndex : uri.length(),
+                fragmentIndex >= 0 ? fragmentIndex : uri.length()
         );
🤖 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 85 - 87, The
substring used to strip query/fragment in StaticFileHandler is treating index 0
as "not found", so URIs starting with '?' or '#' (e.g., "?x=1") are not
sanitized; update the logic that computes the end index for the URI substring
(the code using queryIndex and fragmentIndex) to treat an index >= 0 as a found
delimiter (i.e., use queryIndex >= 0 ? queryIndex : uri.length() and
fragmentIndex >= 0 ? fragmentIndex : uri.length()), then recompute the min of
those two values before substringing to ensure leading '?' or '#' are correctly
stripped.

108-117: ⚠️ Potential issue | 🟠 Major

Path-jail check is still bypassable via symlinks.

Line 112-116 performs a lexical containment check only; a symlink inside webRoot can still resolve to a target outside webRoot.

🛡️ Proposed fix
     private boolean isPathTraversal(String uri) {
         try {
-            Path webRootPath = Paths.get(webRoot).toAbsolutePath().normalize();
-            Path requestedPath = webRootPath.resolve(uri).normalize();
+            Path webRootPath = Paths.get(webRoot).toFile().getCanonicalFile().toPath();
+            Path requestedPath = webRootPath.resolve(uri).toFile().getCanonicalFile().toPath();

             return !requestedPath.startsWith(webRootPath);
         } catch (Exception e) {
             LOGGER.log(Level.WARNING, "Sökvägstraversalkontroll misslyckades för: " + uri, e);
             return true;
         }
     }
🤖 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 108 - 117, The
lexical check in isPathTraversal (webRootPath/requestedPath) can be bypassed by
symlinks; change the function to compare real (canonical) paths instead: call
toRealPath() on the web root (e.g., realWebRoot =
Paths.get(webRoot).toRealPath()) and obtain the real path of the requested
target by resolving uri against the web root and then calling toRealPath() on
the nearest existing ancestor if the final target may not exist (find existing
parent via Files.exists and climb up until exists, call toRealPath() on that
parent and reapply the non-existing remainder) to reconstruct the canonical
requested path; finally return true if the canonical requested path does not
startWith the canonical web root. Ensure all references use the isPathTraversal,
webRootPath, requestedPath, realWebRoot identifiers.
src/main/java/org/example/FileCache.java (1)

62-66: ⚠️ Potential issue | 🟡 Minor

Guard utilization formatting for maxBytes == 0.

Line 65 can output Infinity%/NaN% when maxBytes is zero.

🛡️ Proposed fix
             return String.format(
                     "CacheStats{entries=%d/%d, bytes=%s/%s, utilization=%.1f%%, accesses=%d}",
                     entries, maxEntries, bytesFormatted, maxBytesFormatted,
-                    (double) bytes / maxBytes * 100, totalAccesses
+                    maxBytes > 0 ? (double) bytes / maxBytes * 100 : 0.0, totalAccesses
             );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/FileCache.java` around lines 62 - 66, The
CacheStats string formatting in FileCache can produce Infinity/NaN when maxBytes
== 0; update the code that builds the utilization value (used inside the
String.format call) to guard against division by zero by computing a safe
utilization double first (e.g., if maxBytes == 0 then set utilization to 0.0
else compute (double) bytes / maxBytes * 100) and use that local variable in the
String.format invocation so the log never prints Infinity/NaN; reference the
FileCache method that returns the formatted CacheStats (the String.format call
using entries, maxEntries, bytesFormatted, maxBytesFormatted, bytes, maxBytes,
totalAccesses).
🤖 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/CacheFilter.java`:
- Around line 160-163: The getStats() method currently returns null causing
NPEs; update CacheFilter.getStats() to return a non-null FileCache.CacheStats
instance (either by delegating to the underlying cache/stats provider used
elsewhere in CacheFilter or by constructing and returning an empty/default
FileCache.CacheStats) and ensure all other methods in the same block (lines
around the getStats implementation, e.g., methods referenced between lines
166-199) similarly avoid returning null by using the same underlying stats
source or default objects; locate and use the same cache field or stats helper
used elsewhere in CacheFilter to populate the returned stats.

---

Duplicate comments:
In `@src/main/java/org/example/FileCache.java`:
- Around line 62-66: The CacheStats string formatting in FileCache can produce
Infinity/NaN when maxBytes == 0; update the code that builds the utilization
value (used inside the String.format call) to guard against division by zero by
computing a safe utilization double first (e.g., if maxBytes == 0 then set
utilization to 0.0 else compute (double) bytes / maxBytes * 100) and use that
local variable in the String.format invocation so the log never prints
Infinity/NaN; reference the FileCache method that returns the formatted
CacheStats (the String.format call using entries, maxEntries, bytesFormatted,
maxBytesFormatted, bytes, maxBytes, totalAccesses).

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 85-87: The substring used to strip query/fragment in
StaticFileHandler is treating index 0 as "not found", so URIs starting with '?'
or '#' (e.g., "?x=1") are not sanitized; update the logic that computes the end
index for the URI substring (the code using queryIndex and fragmentIndex) to
treat an index >= 0 as a found delimiter (i.e., use queryIndex >= 0 ? queryIndex
: uri.length() and fragmentIndex >= 0 ? fragmentIndex : uri.length()), then
recompute the min of those two values before substringing to ensure leading '?'
or '#' are correctly stripped.
- Around line 108-117: The lexical check in isPathTraversal
(webRootPath/requestedPath) can be bypassed by symlinks; change the function to
compare real (canonical) paths instead: call toRealPath() on the web root (e.g.,
realWebRoot = Paths.get(webRoot).toRealPath()) and obtain the real path of the
requested target by resolving uri against the web root and then calling
toRealPath() on the nearest existing ancestor if the final target may not exist
(find existing parent via Files.exists and climb up until exists, call
toRealPath() on that parent and reapply the non-existing remainder) to
reconstruct the canonical requested path; finally return true if the canonical
requested path does not startWith the canonical web root. Ensure all references
use the isPathTraversal, webRootPath, requestedPath, realWebRoot identifiers.

In `@src/test/java/org/example/StaticFileHandlerTest.java`:
- Around line 93-94: The test spawns a child thread but only captures Exception
in final Exception[] threadError, so AssertionError (which extends
Error/Throwable) can be lost; update the capture to use Throwable (e.g., final
Throwable[] threadError or an AtomicReference<Throwable>) and change the
thread's catch block to catch Throwable instead of Exception, then after joining
rethrow or fail with the captured Throwable (preserving AssertionError) so
assertions in the child thread are surfaced; reference the threadError variable
and the thread's run/catch block where the error is stored and checked.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5b2f44 and 704f8d8.

📒 Files selected for processing (4)
  • src/main/java/org/example/CacheFilter.java
  • src/main/java/org/example/FileCache.java
  • src/main/java/org/example/StaticFileHandler.java
  • src/test/java/org/example/StaticFileHandlerTest.java

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

♻️ Duplicate comments (2)
src/main/java/org/example/StaticFileHandler.java (2)

84-87: Off-by-one: > 0 should be >= 0 for query/fragment index checks.

If the URI begins with ? or # (index 0), > 0 evaluates to false and the delimiter is not stripped, leaving the query/fragment as part of the filename. Use >= 0 (or != -1).

🐛 Proposed fix
         int endIndex = Math.min(
-                queryIndex > 0 ? queryIndex : uri.length(),
-                fragmentIndex > 0 ? fragmentIndex : uri.length()
+                queryIndex >= 0 ? queryIndex : uri.length(),
+                fragmentIndex >= 0 ? fragmentIndex : uri.length()
         );
🤖 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 84 - 87, The
endIndex computation in StaticFileHandler is using `queryIndex > 0` and
`fragmentIndex > 0`, which misses delimiters at position 0 (e.g., URIs starting
with '?' or '#'); update those checks to use `>= 0` (or `!= -1`) so that
queryIndex and fragmentIndex at index 0 are treated as present; modify the
expression that sets endIndex (and any similar checks around
queryIndex/fragmentIndex) to use `>= 0` to ensure the query/fragment are
stripped from the filename correctly.

108-122: normalize() alone does not resolve symlinks — symlink-based traversal is still possible.

The comment at lines 110-111 acknowledges skipping toRealPath() to avoid requiring directory existence, but this leaves the door open for symlink-based escapes if an attacker can create a symlink inside webRoot pointing outside it. The actual file read at line 48 (Files.readAllBytes(new File(webRoot, sanitizedUri).toPath())) will follow symlinks.

If this is an accepted trade-off, consider documenting it more explicitly (e.g., "symlinks inside webRoot are trusted"). Otherwise, calling toRealPath() on the resolved path (and falling through to the existing catch → return true) would close this gap.

🤖 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 108 - 122, The
isPathTraversal method currently uses
Paths.get(...).toAbsolutePath().normalize() which does not resolve symlinks, so
a symlink inside webRoot can still escape when the file is later read with
Files.readAllBytes(new File(webRoot, sanitizedUri).toPath()); either explicitly
resolve symlinks by calling toRealPath() on the resolved path inside
isPathTraversal (and let the existing catch return true on failure) or, if
symlinks inside webRoot are an accepted risk, add a clear comment/docstring in
isPathTraversal and near the file read (Files.readAllBytes(...)) stating that
symlinks in webRoot are trusted and this is an intentional trade-off.
🧹 Nitpick comments (4)
src/main/java/org/example/StaticFileHandler.java (1)

18-23: SHARED_CACHE is hardcoded — prevents dependency injection and testability.

The FileCache interface is a good step (per reviewer feedback), but instantiating it as static final … = new CacheFilter() couples this class to the concrete implementation and makes it impossible to inject alternatives (e.g., for testing or future Redis-backed caches). Consider accepting a FileCache via constructor parameter and managing the shared instance at the server/wiring level.

Based on learnings, FileCache should be dependency-injected into StaticFileHandler rather than instantiated as a static field, to allow for different cache implementations (e.g., in-memory, Redis).

🤖 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 18 - 23,
SHARED_CACHE is currently tightly coupled to CacheFilter because
StaticFileHandler instantiates it as a static final; change to accept a
FileCache via dependency injection instead: remove the static instantiation of
SHARED_CACHE, add a (final) instance field FileCache cache and a constructor
StaticFileHandler(FileCache cache) that assigns it, and update any call sites
that relied on the static SHARED_CACHE to pass a shared FileCache (created at
server/wiring/bootstrap time) so tests can inject mocks or alternate
implementations like Redis-backed caches; keep a default constructor only if you
provide a reasonable default cache via delegation in wiring, not by hardcoding
new CacheFilter() inside StaticFileHandler.
src/main/java/org/example/CacheFilter.java (3)

30-45: CacheEntry.data is a mutable byte[] — callers can corrupt cached content.

CacheEntry stores the raw byte[] reference. Any caller that mutates the returned array (e.g., getOrFetch → consumer modifies the bytes) will silently corrupt the cached data for all subsequent hits. Defensively copy on store and/or on retrieval.

♻️ Defensive copy on store
         CacheEntry(byte[] data) {
-            this.data = data;
+            this.data = data.clone();
             this.lastAccessTime = new AtomicLong(System.currentTimeMillis());
             this.accessCount = new AtomicLong(0);
         }

Alternatively, clone on retrieval in getOrFetch (line 58 / 68) — pick whichever trade-off (memory vs. safety) fits your use case.

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

In `@src/main/java/org/example/CacheFilter.java` around lines 30 - 45, CacheEntry
currently stores a direct reference to a mutable byte[] (field CacheEntry.data)
which allows callers to mutate cached content; to fix, make CacheEntry defensive
by copying the array on creation (in CacheEntry(byte[] data)) and/or by
returning a copy from the cache access path (e.g., in getOrFetch) so callers
never get the original backing array; update CacheEntry constructor to clone the
incoming byte[] and ensure any place that returns CacheEntry.data (notably
getOrFetch) returns a clone of the stored array to prevent external mutation
from corrupting the cache.

156-169: getStats() is not synchronized — snapshot may be inconsistent with concurrent mutations.

cache.size(), currentBytes.get(), and the access-count stream are read without holding writeLock, so a concurrent addToCacheUnsafe/evict can interleave and produce a snapshot where, e.g., entries and bytes don't correspond. For a diagnostics-only method this is likely acceptable — just flagging for awareness.

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

In `@src/main/java/org/example/CacheFilter.java` around lines 156 - 169,
getStats() currently reads cache.size(), currentBytes.get(), and the
access-count stream without locking, allowing concurrent addToCacheUnsafe/evict
to produce an inconsistent snapshot; fix by acquiring the same lock used for
mutations (use writeLock.lock() / writeLock.unlock() around the snapshot read)
so the returned FileCache.CacheStats reflects a consistent state—apply this
change inside getStats() and reference writeLock, getStats, addToCacheUnsafe,
and evict when locating the code to modify.

62-82: All cache misses are serialized through a single global lock — potential throughput bottleneck.

synchronized(writeLock) guards the entire fetch-and-store path. If threads request different, uncached URIs concurrently, they queue behind the same lock — including the disk I/O in provider.fetch(uri). Under load this serializes all first-time file reads.

A per-key locking pattern (e.g., ConcurrentHashMap<String, Future<byte[]>> with computeIfAbsent) would allow concurrent fetches for distinct URIs while still coalescing duplicate requests for the same URI.

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

In `@src/main/java/org/example/CacheFilter.java` around lines 62 - 82, The current
synchronized(writeLock) serializes all cache misses including
provider.fetch(uri); replace the global lock with a per-key in-flight map (e.g.,
ConcurrentHashMap<String, CompletableFuture<byte[]>> or Future) so only
identical URIs are coalesced while distinct URIs fetch concurrently: on lookup
use cache.get(uri) then if missing use inFlight.computeIfAbsent(uri, k ->
CompletableFuture.supplyAsync(() -> { byte[] bytes = provider.fetch(k); if
(bytes != null) addToCacheUnsafe(k, bytes); return bytes; })); wait/join the
future, remove the future from inFlight on completion/exception, and preserve
entry.recordAccess/LOGGER logic when returning cached data; this keeps
addToCacheUnsafe, cache, provider.fetch, writeLock (remove or narrow usage) and
LOGGER references but moves I/O out of the global synchronized block.
🤖 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/StaticFileHandler.java`:
- Around line 36-61: In sendGetRequest, the catch block logs the raw
user-provided uri which can enable log-forging; update the
LOGGER.log(Level.FINE, ...) call in the catch to use sanitizedUri (or an
escaped/normalized representation of it) instead of the unsanitized uri so only
validated/escaped input is logged; locate the LOGGER.log call in sendGetRequest
and replace the logged variable accordingly (optionally apply a simple escape
function before logging).

---

Duplicate comments:
In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 84-87: The endIndex computation in StaticFileHandler is using
`queryIndex > 0` and `fragmentIndex > 0`, which misses delimiters at position 0
(e.g., URIs starting with '?' or '#'); update those checks to use `>= 0` (or `!=
-1`) so that queryIndex and fragmentIndex at index 0 are treated as present;
modify the expression that sets endIndex (and any similar checks around
queryIndex/fragmentIndex) to use `>= 0` to ensure the query/fragment are
stripped from the filename correctly.
- Around line 108-122: The isPathTraversal method currently uses
Paths.get(...).toAbsolutePath().normalize() which does not resolve symlinks, so
a symlink inside webRoot can still escape when the file is later read with
Files.readAllBytes(new File(webRoot, sanitizedUri).toPath()); either explicitly
resolve symlinks by calling toRealPath() on the resolved path inside
isPathTraversal (and let the existing catch return true on failure) or, if
symlinks inside webRoot are an accepted risk, add a clear comment/docstring in
isPathTraversal and near the file read (Files.readAllBytes(...)) stating that
symlinks in webRoot are trusted and this is an intentional trade-off.

---

Nitpick comments:
In `@src/main/java/org/example/CacheFilter.java`:
- Around line 30-45: CacheEntry currently stores a direct reference to a mutable
byte[] (field CacheEntry.data) which allows callers to mutate cached content; to
fix, make CacheEntry defensive by copying the array on creation (in
CacheEntry(byte[] data)) and/or by returning a copy from the cache access path
(e.g., in getOrFetch) so callers never get the original backing array; update
CacheEntry constructor to clone the incoming byte[] and ensure any place that
returns CacheEntry.data (notably getOrFetch) returns a clone of the stored array
to prevent external mutation from corrupting the cache.
- Around line 156-169: getStats() currently reads cache.size(),
currentBytes.get(), and the access-count stream without locking, allowing
concurrent addToCacheUnsafe/evict to produce an inconsistent snapshot; fix by
acquiring the same lock used for mutations (use writeLock.lock() /
writeLock.unlock() around the snapshot read) so the returned
FileCache.CacheStats reflects a consistent state—apply this change inside
getStats() and reference writeLock, getStats, addToCacheUnsafe, and evict when
locating the code to modify.
- Around line 62-82: The current synchronized(writeLock) serializes all cache
misses including provider.fetch(uri); replace the global lock with a per-key
in-flight map (e.g., ConcurrentHashMap<String, CompletableFuture<byte[]>> or
Future) so only identical URIs are coalesced while distinct URIs fetch
concurrently: on lookup use cache.get(uri) then if missing use
inFlight.computeIfAbsent(uri, k -> CompletableFuture.supplyAsync(() -> { byte[]
bytes = provider.fetch(k); if (bytes != null) addToCacheUnsafe(k, bytes); return
bytes; })); wait/join the future, remove the future from inFlight on
completion/exception, and preserve entry.recordAccess/LOGGER logic when
returning cached data; this keeps addToCacheUnsafe, cache, provider.fetch,
writeLock (remove or narrow usage) and LOGGER references but moves I/O out of
the global synchronized block.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 18-23: SHARED_CACHE is currently tightly coupled to CacheFilter
because StaticFileHandler instantiates it as a static final; change to accept a
FileCache via dependency injection instead: remove the static instantiation of
SHARED_CACHE, add a (final) instance field FileCache cache and a constructor
StaticFileHandler(FileCache cache) that assigns it, and update any call sites
that relied on the static SHARED_CACHE to pass a shared FileCache (created at
server/wiring/bootstrap time) so tests can inject mocks or alternate
implementations like Redis-backed caches; keep a default constructor only if you
provide a reasonable default cache via delegation in wiring, not by hardcoding
new CacheFilter() inside StaticFileHandler.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 704f8d8 and 3f107c6.

📒 Files selected for processing (3)
  • src/main/java/org/example/CacheFilter.java
  • src/main/java/org/example/FileCache.java
  • src/main/java/org/example/StaticFileHandler.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/org/example/FileCache.java

Copy link

@donne41 donne41 left a comment

Choose a reason for hiding this comment

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

Jag testade att göra om den lite grann med att sätta new CacheFilter i TcpServer, och skicka vidare den i kedjan. Då funkar cachen att den sparar och hämtar som den ska najs!

Dock så får jag erkänna att jag glömde vad det stod i Issuen, så efter uppfriskat minne så kan jag säga att den inte gör sitt jobb som den är nu.

Men det är inte långt borta. Förslag till lösning är
flytta var cachen ska leva någonstans
få uri i ett tidigt stadie
om den finns i cachen retunera det direkt
annars får staticFilehandler läsa in det och spara till cache.

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

♻️ Duplicate comments (4)
src/main/java/org/example/StaticFileHandler.java (2)

20-22: Prefer injecting a cache interface instead of using static FileCache calls.

Current usage hard-couples StaticFileHandler to one global in-memory implementation and makes alternative backends/lifecycle control harder.

Based on learnings: Refactor FileCache to an interface to support multiple cache implementations and inject it into StaticFileHandler via dependency injection.

Also applies to: 33-38, 85-87

🤖 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 20 - 22,
StaticFileHandler is tightly coupled to the global FileCache; refactor to accept
a FileCache interface instance via dependency injection: add a constructor
parameter (e.g., FileCache cache) and store it as a private final field, replace
all static FileCache calls in StaticFileHandler (constructor, methods referenced
around lines 33-38 and 85-87) with instance calls on that field
(cache.get/put/remove or appropriately named methods), and update callers to
pass the desired FileCache implementation (in-memory, noop, or lifecycle-managed
implementation). Ensure FileCache is an interface (if not already) with the
methods StaticFileHandler uses and update imports/tests accordingly.

66-73: ⚠️ Potential issue | 🟠 Major

Path traversal check is still bypassable through symlinks.

normalize() does not resolve symlink targets for the requested path. Resolve both root and requested path with toRealPath() before startsWith(...).

🛡️ Proposed fix
     private boolean isPathTraversal(String uri) {
         try {
-            Path webRootPath = Paths.get(webRoot).toAbsolutePath().normalize();
-            Path requestedPath = webRootPath.resolve(uri).normalize();
-            return !requestedPath.startsWith(webRootPath);
-        } catch (Exception e) {
+            Path webRootPath = Paths.get(webRoot).toRealPath();
+            Path resolvedPath = webRootPath.resolve(uri).normalize();
+            Path realRequestedPath = resolvedPath.toRealPath();
+            return !realRequestedPath.startsWith(webRootPath);
+        } catch (IOException e) {
             return true;
         }
     }
🤖 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 66 - 73, The
isPathTraversal method currently uses normalize() which doesn't resolve
symlinks; update it to call toRealPath() on both the webRoot (webRootPath) and
the resolved requestedPath before comparing with startsWith to ensure symlink
targets are resolved; handle IOException thrown by toRealPath() and treat
failures as traversal (return true) while keeping the existing exception-safe
behavior.
src/main/java/org/example/FileCache.java (1)

8-14: ⚠️ Potential issue | 🟠 Major

Defensive-copy cached byte arrays to prevent cache data corruption.

put stores caller-owned byte[] and get returns internal references. Any mutation by callers can silently corrupt cached responses across requests.

🛠️ Proposed fix
     public static byte[] get(String key) {
-        return cache.get(key);
+        byte[] value = cache.get(key);
+        return value == null ? null : value.clone();
     }

     public static void put(String key, byte[] content) {
-        cache.put(key, content);
+        if (content == null) {
+            cache.remove(key);
+            return;
+        }
+        cache.put(key, content.clone());
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/FileCache.java` around lines 8 - 14, FileCache's
put and get expose internal byte[] via cache, allowing callers to mutate cached
data; make defensive copies by copying the input byte[] in put (store a
clone/copy into cache) and returning a copy from get (do not return the internal
reference), handle null inputs/returns appropriately; update methods named put
and get and the cache usage so the internal cache only holds and returns copies
to avoid shared-mutable state.
src/test/java/org/example/StaticFileHandlerTest.java (1)

82-114: ⚠️ Potential issue | 🟠 Major

Concurrency test does not wait for threads and can miss worker failures.

The test starts threads but never join()s them, and catch (Exception) will not capture AssertionError. This can produce false positives.

🧪 Proposed fix
-        final Exception[] threadError = new Exception[1];
+        final Throwable[] threadError = new Throwable[1];

         for (int i = 0; i < 10; i++) {
             threads[i] = new Thread(() -> {
                 try {
                     for (int j = 0; j < 50; j++) {
                         ByteArrayOutputStream out = new ByteArrayOutputStream();
                         handler.sendGetRequest(out, "shared.html");
                         String response = out.toString();

                         if (!response.contains("HTTP/1.1 200") || !response.contains("Data")) {
                             throw new AssertionError("Oväntad response");
                         }
                     }
-                } catch (Exception e) {
+                } catch (Throwable e) {
                     synchronized (threads) {
-                        threadError[0] = e;
+                        if (threadError[0] == null) {
+                            threadError[0] = e;
+                        }
                     }
                 }
             });
             threads[i].start();
         }
+
+        for (Thread thread : threads) {
+            thread.join();
+        }
+
+        if (threadError[0] != null) {
+            fail("Worker thread failed", threadError[0]);
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/org/example/StaticFileHandlerTest.java` around lines 82 - 114,
The test testConcurrent_MultipleReads starts threads but never waits for them
and also only catches Exception (missing AssertionError), so update the thread
handling in this test: after starting all threads, loop over the threads array
and call join() on each to wait for completion; change the thread's catch to
capture AssertionError as well (e.g., catch (Throwable t) or catch (Exception |
AssertionError t)) and store it into threadError; after all joins check
threadError[0] and rethrow or fail the test if non-null so worker failures cause
the test to fail.
🤖 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/FileCache.java`:
- Line 6: FileCache currently uses a static unbounded ConcurrentHashMap named
cache which can grow until OOM under unique-path traffic; cap memory by
replacing or wrapping cache with a bounded, eviction-capable structure (e.g.,
use a Caffeine cache or implement a LRU using LinkedHashMap with accessOrder
plus size checks) and enforce either maxEntries or maxBytes and an eviction
policy. Modify the FileCache class to initialize cache with a bounded capacity
or a cache builder (respecting eviction on access/write), update any
put/get/remove usage to work with the chosen cache API, and ensure eviction
metrics/logging are present so entries are evicted deterministically instead of
unbounded accumulation.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 32-49: The current catch around reading, building and writing the
response turns any IOException (including outputStream.write/flush failures)
into a 404; narrow the error handling so only file-missing exceptions produce
404. Surround the file-read/cache logic (use FileCache, Files.readAllBytes and
sanitizedUri) with a try that catches NoSuchFileException/FileNotFoundException
and calls writeResponse(outputStream, 404, "Not Found"); use a separate catch
for other IOExceptions (including failures from HttpResponseBuilder.build(),
outputStream.write(), outputStream.flush()) that logs the error and returns a
500 via writeResponse(outputStream, 500, "Internal Server Error"). Ensure
exceptions are handled at the appropriate scope so write failures are not
reported as missing files.

---

Duplicate comments:
In `@src/main/java/org/example/FileCache.java`:
- Around line 8-14: FileCache's put and get expose internal byte[] via cache,
allowing callers to mutate cached data; make defensive copies by copying the
input byte[] in put (store a clone/copy into cache) and returning a copy from
get (do not return the internal reference), handle null inputs/returns
appropriately; update methods named put and get and the cache usage so the
internal cache only holds and returns copies to avoid shared-mutable state.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 20-22: StaticFileHandler is tightly coupled to the global
FileCache; refactor to accept a FileCache interface instance via dependency
injection: add a constructor parameter (e.g., FileCache cache) and store it as a
private final field, replace all static FileCache calls in StaticFileHandler
(constructor, methods referenced around lines 33-38 and 85-87) with instance
calls on that field (cache.get/put/remove or appropriately named methods), and
update callers to pass the desired FileCache implementation (in-memory, noop, or
lifecycle-managed implementation). Ensure FileCache is an interface (if not
already) with the methods StaticFileHandler uses and update imports/tests
accordingly.
- Around line 66-73: The isPathTraversal method currently uses normalize() which
doesn't resolve symlinks; update it to call toRealPath() on both the webRoot
(webRootPath) and the resolved requestedPath before comparing with startsWith to
ensure symlink targets are resolved; handle IOException thrown by toRealPath()
and treat failures as traversal (return true) while keeping the existing
exception-safe behavior.

In `@src/test/java/org/example/StaticFileHandlerTest.java`:
- Around line 82-114: The test testConcurrent_MultipleReads starts threads but
never waits for them and also only catches Exception (missing AssertionError),
so update the thread handling in this test: after starting all threads, loop
over the threads array and call join() on each to wait for completion; change
the thread's catch to capture AssertionError as well (e.g., catch (Throwable t)
or catch (Exception | AssertionError t)) and store it into threadError; after
all joins check threadError[0] and rethrow or fail the test if non-null so
worker failures cause the test to fail.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f107c6 and d87171b.

📒 Files selected for processing (3)
  • src/main/java/org/example/FileCache.java
  • src/main/java/org/example/StaticFileHandler.java
  • src/test/java/org/example/StaticFileHandlerTest.java

@Ericthilen Ericthilen self-requested a review March 1, 2026 18:54
Copy link

@Ericthilen Ericthilen left a comment

Choose a reason for hiding this comment

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

Vissa commits är overifierade men det kanske inte behövs då det går att ta bort det kravet.

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.

Server‑side caching (in‑memory cache)

5 participants