Skip to content

Implement-file-streaming-for-large-files-#68#91

Open
Boppler12 wants to merge 28 commits intomainfrom
Implement-file-streaming-for-large-files-#68
Open

Implement-file-streaming-for-large-files-#68#91
Boppler12 wants to merge 28 commits intomainfrom
Implement-file-streaming-for-large-files-#68

Conversation

@Boppler12
Copy link

@Boppler12 Boppler12 commented Feb 25, 2026

Summary by CodeRabbit

  • New Features

    • File caching and smart file delivery: small files cached, large files streamed to reduce memory use
    • Automatic MIME/content-type detection for served files
    • Configurable web root and runtime port resolution (CLI/config/default)
    • Request filtering: IP-based allow/block and locale extraction
    • Improved HTTP response handling and streaming robustness
  • Documentation

    • Expanded README and new PortConfigurationGuide.md
  • Tests

    • Extensive new unit/integration tests covering caching, file serving, filters, MIME detection, and response building
  • Assets

    • Added a custom 404 page for nicer error responses

@coderabbitai
Copy link

coderabbitai bot commented Feb 25, 2026

Warning

Rate limit exceeded

@Boppler12 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 45 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 1c57ca3 and e8781bd.

📒 Files selected for processing (5)
  • src/main/java/org/example/CacheFilter.java
  • src/main/java/org/example/FileCache.java
  • src/main/java/org/example/StaticFileHandler.java
  • src/main/java/org/example/http/HttpResponseBuilder.java
  • src/test/java/org/example/StaticFileHandlerTest.java
📝 Walkthrough

Walkthrough

Adds a thread-safe LRU FileCache and URI-based CacheFilter, refactors StaticFileHandler for configurable roots, streaming and cache paths, expands HttpResponseBuilder, introduces filter pipeline (Filter, FilterChain, IpFilter, LocaleFilter, FilterChainImpl), request/config infrastructure (HttpRequest, AppConfig, ConfigLoader), MIME detector, many tests, Dockerfile and docs updates.

Changes

Cohort / File(s) Summary
Caching
src/main/java/org/example/FileCache.java, src/main/java/org/example/CacheFilter.java
New thread-safe LRU FileCache with size tracking and eviction; CacheFilter provides get-or-fetch semantics via a FileProvider functional interface and logs hits/misses.
Static file serving
src/main/java/org/example/StaticFileHandler.java, www/pageNotFound.html
Refactored StaticFileHandler: configurable web root, URI sanitization, directory-traversal protection, dual path for small (cached) vs large (streamed) files, and error responses; adds static 404 page.
HTTP response & MIME
src/main/java/org/example/http/HttpResponseBuilder.java, src/main/java/org/example/http/MimeTypeDetector.java
HttpResponseBuilder extended with status constants, case-insensitive headers, unified header/body handling and buildHeaders(); MimeTypeDetector utility added for content-type inference.
Filter pipeline
src/main/java/org/example/filter/*
New Filter, FilterChain, FilterChainImpl interfaces/impl and filters: IpFilter (allow/block modes) and LocaleFilter (Accept-Language handling).
Request & connection changes
src/main/java/org/example/httpparser/HttpRequest.java, src/main/java/org/example/ConnectionHandler.java
New immutable HttpRequest with attribute support; ConnectionHandler now builds and applies filter chain, injects client IP attribute, and delegates to StaticFileHandler.
Configuration
src/main/java/org/example/config/AppConfig.java, src/main/java/org/example/config/ConfigLoader.java, src/main/resources/application.yml, PortConfigurationGuide.md
New AppConfig record model with nested configs and defaults; ConfigLoader for loading/caching YAML/JSON configs with defaults and test reset; default application.yml and port guide added.
App & CLI
src/main/java/org/example/App.java
App now loads config and resolves port from CLI --port or config with validation and explicit error handling.
Tests
src/test/java/org/example/*, src/test/java/org/example/filter/*, src/test/java/org/example/http/*
Large set of new and expanded tests: FileCacheTest, StaticFileHandlerTest, AppPortResolutionTest, ConfigLoaderTest, IpFilterTest, LocaleFilterTest, HttpResponseBuilderTest, MimeTypeDetectorTest, and related test helpers; cover many edge cases and integration scenarios.
Build & docs
Dockerfile, README.md
Dockerfile adjusted to copy dependencies and assets into /app and expose 8080; README rewritten with full server docs and Quick Start.

Sequence Diagram

sequenceDiagram
    participant Client
    participant StaticFileHandler
    participant CacheFilter
    participant FileCache
    participant FileProvider as Disk

    Client->>StaticFileHandler: GET /path
    activate StaticFileHandler

    alt fileSize < threshold
        StaticFileHandler->>CacheFilter: getOrFetch(uri, provider)
        activate CacheFilter

        CacheFilter->>FileCache: get(uri)
        activate FileCache
        FileCache-->>CacheFilter: bytes or null
        deactivate FileCache

        alt cache hit
            CacheFilter-->>StaticFileHandler: bytes
        else cache miss
            CacheFilter->>FileProvider: fetch(uri)
            activate FileProvider
            FileProvider-->>CacheFilter: bytes
            deactivate FileProvider

            CacheFilter->>FileCache: put(uri, bytes)
            activate FileCache
            FileCache-->>CacheFilter: done
            deactivate FileCache

            CacheFilter-->>StaticFileHandler: bytes
        end
        deactivate CacheFilter
    else stream large file
        StaticFileHandler->>FileProvider: open & stream chunks
        activate FileProvider
        FileProvider-->>StaticFileHandler: byte chunks
        deactivate FileProvider
    end

    StaticFileHandler->>Client: send headers + body/stream
    deactivate StaticFileHandler
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

priority:high

Suggested reviewers

  • gvaguirres
  • AntonAhlqvist

Poem

🐰 I dug a cache beneath the tree,
Small files snuggle safe with me,
Big streams ripple, fast and free,
Filters guard the gate, you see—
Hops and bytes in harmony. 🥕

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Title check ⚠️ Warning The PR title focuses on implementing file streaming for large files, which is a major component of the changes. However, this PR contains extensive additional changes including a complete caching system (FileCache, CacheFilter), configuration management (AppConfig, ConfigLoader), HTTP response improvements, filter architecture (Filter, FilterChain, IpFilter, LocaleFilter), MIME type detection, and comprehensive test coverage. The title captures only one aspect of a much broader refactoring. Expand the title to reflect the scope of changes, such as 'Add file streaming, caching, configuration, and filter architecture' or provide a more specific single-focus title if this PR should be split into smaller changes.
Docstring Coverage ⚠️ Warning Docstring coverage is 11.93% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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 Implement-file-streaming-for-large-files-#68

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

🧹 Nitpick comments (10)
src/main/java/org/example/FileCache.java (2)

14-14: Consider extracting a FileCache interface for dependency injection.

The current concrete class makes it difficult to swap implementations (e.g., for testing or alternative backends). Based on learnings, FileCache should be refactored to 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/FileCache.java` at line 14, Extract a FileCache
interface and refactor the concrete class into an implementation (e.g.,
FileCacheImpl) so the cache can be swapped via DI: create a public interface
named FileCache exposing the methods currently used by the class (e.g., get,
put, invalidate or whatever methods exist on the concrete FileCache), rename the
existing class to FileCacheImpl (or keep it implementing FileCache) and
implement the interface, then modify StaticFileHandler to accept a FileCache in
its constructor (or via setter) and use the interface type instead of the
concrete class so tests or alternate backends (in-memory, Redis) can be
injected.

101-109: CacheEntry.timestamp is stored but never read.

The timestamp field is set in the constructor but is not used anywhere in the cache logic (no TTL expiration, no reporting). Consider removing it to avoid confusion, or implementing TTL-based expiration if that was the intent.

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

In `@src/main/java/org/example/FileCache.java` around lines 101 - 109, The
CacheEntry.timestamp field is assigned but never used; either remove that field
and its assignment in the CacheEntry constructor, or implement TTL-based
expiration by using CacheEntry.timestamp: add a TTL constant (e.g.,
CACHE_TTL_MS) and update the cache access methods (e.g., the get(...) and any
existing put(...) or cleanup methods) to check System.currentTimeMillis() -
entry.timestamp > CACHE_TTL_MS, evict expired entries before returning values,
and consider a periodic cleanup or lazy removal on access; reference the
CacheEntry class and its constructor, and the cache get/put methods to locate
where to apply the change.
src/test/java/org/example/FileCacheTest.java (1)

7-117: Consider adding tests for LRU eviction behavior.

The test suite covers core CRUD operations well but does not exercise the eviction logic (MAX_ENTRIES or MAX_CACHE_SIZE limits). An eviction test would have caught the currentSize drift bug in FileCache.removeEldestEntry. For example, insert entries that exceed the 50 MB limit and assert that older entries are evicted and getCurrentSizeInBytes() remains bounded.

🤖 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 - 117, Add a new
unit test that exercises FileCache's LRU eviction by inserting enough entries to
exceed MAX_CACHE_SIZE (and/or MAX_ENTRIES) and asserting older keys are evicted
and getCurrentSizeInBytes() stays <= MAX_CACHE_SIZE; specifically target the
eviction path exercised by FileCache.removeEldestEntry and verify both
contains(key) and get(key) for evicted vs retained keys and that
getCurrentSizeInBytes() does not drift upward after multiple evictions to catch
the bug in currentSize accounting.
src/main/java/org/example/http/HttpResponseBuilder.java (2)

46-57: Potential duplicate Content-Length / Connection headers if set in the headers map.

buildHeaders() unconditionally appends Content-Length and Connection: close after iterating headers. If a caller also includes "Content-Length" or "Connection" in the headers map (via setHeaders), the response will contain duplicate headers. Current callers in StaticFileHandler only set Content-Type, so this isn't broken today, but the API is fragile.

Consider either filtering those keys from headers before iteration, or documenting the contract that callers must not set them.

🤖 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 46 -
57, The buildHeaders() method always appends "Content-Length" and "Connection:
close" after iterating the headers map, causing duplicate headers if callers put
those keys into the headers map; update buildHeaders() to filter out/ignore
"Content-Length" and "Connection" (case-insensitively) when iterating headers
(or alternatively remove them from the headers map earlier, e.g., in
setHeaders), so only the canonical values appended by buildHeaders() appear;
ensure you reference the headers map and REASON_PHRASES/statusCode logic in
buildHeaders(), and adjust or document behavior used by callers like
StaticFileHandler to avoid duplicates.

60-68: Local variable contentLength shadows the instance field.

The int contentLength declared at line 62 shadows this.contentLength (long). This works correctly because build() computes its own value, but it's confusing and could lead to subtle bugs if someone later tries to unify the two code paths.

🤖 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 60 -
68, The method build() currently declares a local int contentLength that shadows
the instance field contentLength; remove the local variable and assign the
computed length directly to the instance field (this.contentLength) so both code
paths update the same field. In build(), when body is empty and bytebody != null
setBody(new String(bytebody, StandardCharsets.UTF_8)) and then assign
this.contentLength = bytebody.length; otherwise compute this.contentLength =
body.getBytes(StandardCharsets.UTF_8).length (promote to long if needed) so the
class-level contentLength consistently reflects the computed value; adjust any
downstream code expecting a long accordingly.
src/test/java/org/example/StaticFileHandlerTest.java (2)

206-244: Caching/non-caching tests only verify both calls succeed — they don't verify cache behavior.

testSmallFileUsesCaching and testLargeFileNotCached both assert that two consecutive calls return 200 OK, which would pass regardless of whether caching is active. To actually verify cache hit/miss behavior, consider injecting a mock or spy CacheFilter and asserting the expected interactions (e.g., fetch called once for cached, twice for non-cached), or at minimum delete the file between calls to confirm the second request is served from cache.

🤖 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 - 244,
Update the two tests to actually verify cache behavior rather than just 200
responses: in testSmallFileUsesCaching, replace or wrap the real CacheFilter
with a mock/spy (or inject a test double) and assert that the cache's
fetch/store methods (e.g., CacheFilter.fetch or CacheFilter.get/put) are called
only once across the two handler.sendGetRequest calls for "cached.html";
alternatively, after the first successful request delete the underlying file and
assert the second request still returns 200 to prove the response came from
cache. In testLargeFileNotCached, similarly use a mock/spy CacheFilter and
assert that fetch/get is invoked for every handler.sendGetRequest call (i.e.,
called twice), or keep the file and verify that removing the file before the
second request causes the second request to fail if not cached (to prove no
caching). Ensure the tests reference handler.sendGetRequest and the CacheFilter
methods used by your server to locate instrumentation points.

148-171: Memory-usage test is inherently flaky.

Runtime.totalMemory() - freeMemory() is a coarse snapshot that is heavily influenced by GC timing, other tests, and the ByteArrayOutputStream itself (which buffers the entire 15 MB response in-heap). The 50 MB threshold is generous, but this test can still intermittently fail or pass vacuously depending on JVM state. Consider either removing it or replacing it with a deterministic approach (e.g., a counting OutputStream that tracks bytes written without buffering the full payload).

🤖 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 148 - 171,
The test testStreamingFilesDoNotExceedMemoryThreshold is flaky because it
measures JVM heap via Runtime and uses ByteArrayOutputStream which buffers the
whole payload; replace the memory-based assertion with a deterministic check:
use a non-buffering counting OutputStream (implement a simple OutputStream that
increments a long counter on write) instead of ByteArrayOutputStream, call
handler.sendGetRequest(output, "memory_test.bin"), assert the counter equals the
expected file length and remove the Runtime memory measurements and the 50MB
assertion; reference the test method name
testStreamingFilesDoNotExceedMemoryThreshold and handler.sendGetRequest when
making the change.
src/main/java/org/example/StaticFileHandler.java (2)

122-131: Lambda ignores its path parameter, capturing the outer file instead.

The FileProvider.fetch(String uri) lambda at line 124 receives a path argument but ignores it, using the captured file variable. This makes the FileProvider interface misleading — it suggests the provider resolves the URI, but the actual resolution happened earlier. If the cache is ever shared across contexts, the wrong file could be served.

♻️ Suggested clarification
         byte[] fileBytes = cacheFilter.getOrFetch(uri,
-                path -> Files.readAllBytes(file.toPath())
+                ignored -> Files.readAllBytes(file.toPath())
         );

Or better, have the provider actually use its argument by passing the resolved path:

         byte[] fileBytes = cacheFilter.getOrFetch(uri,
-                path -> Files.readAllBytes(file.toPath())
+                u -> Files.readAllBytes(new File(webRoot, u).toPath())
         );
🤖 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 122 - 131, The
lambda passed to cacheFilter.getOrFetch in sendSmallFile currently ignores its
parameter and closes over the outer file, causing the provider to always read
the same File; change the provider so it actually uses its argument (the
FileProvider.fetch(String path) parameter) to read bytes (e.g., use
Files.readAllBytes(Paths.get(path)) or new File(path).toPath()) and ensure the
key you pass (uri) matches the path semantics expected by getOrFetch so the
cached entry corresponds to the provided path; update sendSmallFile,
cacheFilter.getOrFetch call, and the lambda to reference the path parameter
instead of the captured file.

97-120: Consider using Files.probeContentType() or a map-based lookup instead of an if-else chain.

The manual if-else chain for content types works but is verbose and hard to extend. A static Map<String, String> keyed by extension would be more maintainable.

🤖 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 97 - 120,
Replace the long if-else in StaticFileHandler.getContentType with a static
Map<String,String> (e.g. EXTENSION_CONTENT_TYPE) that maps extensions like
"html","css","js","png", etc. to their MIME types; initialize the map in a
static block or as an immutable map, then in getContentType extract the file
extension from the uri, look it up in EXTENSION_CONTENT_TYPE and return the
mapped value if present; if not found, call Files.probeContentType(path) as a
fallback (catching IOExceptions) and finally default to
"application/octet-stream".
src/main/java/org/example/CacheFilter.java (1)

7-7: FileCache is hardcoded; consider injecting it.

CacheFilter instantiates its own FileCache internally, making it impossible to share or swap the cache implementation. Since CacheFilter is already injected into StaticFileHandler, accepting FileCache as a constructor parameter would complete the DI chain. Based on learnings, FileCache should be dependency-injected and ideally be an interface.

🤖 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 7, CacheFilter currently
constructs a concrete FileCache instance (private final FileCache cache = new
FileCache()), preventing DI and substitution; change CacheFilter to accept a
FileCache (preferably an interface type) via its constructor and store it as a
final field instead of new-ing it. Update the StaticFileHandler constructor
where CacheFilter is injected (or where CacheFilter is created) to pass the
shared FileCache implementation (or an implementation bound in your DI setup).
Ensure FileCache is represented as an interface (e.g., FileCache ->
FileCacheInterface) or an abstract type so different implementations 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/CacheFilter.java`:
- Around line 13-16: In CacheFilter (used by sendSmallFile) avoid the TOCTOU by
removing the separate contains(uri) call and instead call cache.get(uri) once,
treating a non-null return as a cache hit: call cache.get(uri), if the returned
value is non-null call logCacheHit(uri) and return that value; do not call
contains(uri) or rely on it, since FileCache.put()/LRU eviction can remove the
entry between contains() and get().

In `@src/main/java/org/example/FileCache.java`:
- Around line 22-29: The LRU eviction currently never decrements the
FileCache.currentSize when the anonymous LinkedHashMap.removeEldestEntry evicts
an entry; update the override in the FileCache() constructor to retrieve the
evicted entry via eldest.getValue(), compute its size (e.g., entry.data.length
or entry.getSize()), subtract that amount from currentSize before returning
true, and ensure you null-check eldest.getValue() to avoid NPE; keep the
original eviction condition (cache.size() > MAX_ENTRIES || currentSize >
MAX_CACHE_SIZE) but perform the currentSize adjustment just prior to returning
true so the cache size accounting stays correct.
- Around line 42-50: The get(String key) method in FileCache currently acquires
lock.readLock() but calling cache.get(key) on an access-ordered LinkedHashMap
mutates its ordering; change the method to use lock.writeLock() (acquire/release
the write lock instead of the read lock) around the lookup to prevent concurrent
structural modifications of cache (the LinkedHashMap) and ensure safe access to
CacheEntry and entry.data; keep the null check (Objects.requireNonNull(key)) and
the existing try/finally pattern but swap readLock() calls to writeLock() calls
on the lock field.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 40-63: The current sendGetRequest sends headers via
sendHttpHeaders before reading the body, which allows a 200+Content-Length to
reach the client even if reading the file later fails; change the small-file
path (file.length() < FILE_SIZE_THRESHOLD) to read the file bytes into memory
first (e.g., use the same logic currently in sendSmallFile but perform the read
in sendGetRequest), then call sendHttpHeaders and write the bytes so headers
reflect a successfully-read body, and for the large-file path wrap the
sendLargeFile call in a try-catch inside sendGetRequest (or ensure sendLargeFile
itself catches IOExceptions) to log the error and avoid swallowing exceptions
that would produce silent truncation; reference sendGetRequest, sendHttpHeaders,
sendSmallFile, sendLargeFile and FILE_SIZE_THRESHOLD when making the changes.
- Line 47: The File is being constructed with a URI that may start with a
leading slash so new File(webRoot, uri) becomes an absolute path and bypasses
the webRoot; update resolveTargetFile()/sendGetRequest() flow to strip any
leading '/' (e.g., normalize uri = uri.replaceFirst("^/+", "")) before
constructing the File (the line "File file = new File(webRoot, uri)") and then
proceed to verifyFileIsWithinWebRoot(file) so legitimate requests like
"/index.html" resolve under webRoot; ensure tests are updated to include URIs
with leading slashes to cover this case.

In `@src/test/java/org/example/StaticFileHandlerTest.java`:
- Around line 103-105: Move the AssertJ failure message configuration before the
evaluation call so the custom message is used: in StaticFileHandlerTest (the
assertion around separatorIndex from findHeaderBodySeparator(outputBytes)),
chain .withFailMessage("Kunde inte hitta HTTP-header separator") immediately
after assertThat(separatorIndex) and before .isGreaterThanOrEqualTo(0) so the
message is applied on failure rather than after the assertion has already
executed.

---

Nitpick comments:
In `@src/main/java/org/example/CacheFilter.java`:
- Line 7: CacheFilter currently constructs a concrete FileCache instance
(private final FileCache cache = new FileCache()), preventing DI and
substitution; change CacheFilter to accept a FileCache (preferably an interface
type) via its constructor and store it as a final field instead of new-ing it.
Update the StaticFileHandler constructor where CacheFilter is injected (or where
CacheFilter is created) to pass the shared FileCache implementation (or an
implementation bound in your DI setup). Ensure FileCache is represented as an
interface (e.g., FileCache -> FileCacheInterface) or an abstract type so
different implementations can be provided.

In `@src/main/java/org/example/FileCache.java`:
- Line 14: Extract a FileCache interface and refactor the concrete class into an
implementation (e.g., FileCacheImpl) so the cache can be swapped via DI: create
a public interface named FileCache exposing the methods currently used by the
class (e.g., get, put, invalidate or whatever methods exist on the concrete
FileCache), rename the existing class to FileCacheImpl (or keep it implementing
FileCache) and implement the interface, then modify StaticFileHandler to accept
a FileCache in its constructor (or via setter) and use the interface type
instead of the concrete class so tests or alternate backends (in-memory, Redis)
can be injected.
- Around line 101-109: The CacheEntry.timestamp field is assigned but never
used; either remove that field and its assignment in the CacheEntry constructor,
or implement TTL-based expiration by using CacheEntry.timestamp: add a TTL
constant (e.g., CACHE_TTL_MS) and update the cache access methods (e.g., the
get(...) and any existing put(...) or cleanup methods) to check
System.currentTimeMillis() - entry.timestamp > CACHE_TTL_MS, evict expired
entries before returning values, and consider a periodic cleanup or lazy removal
on access; reference the CacheEntry class and its constructor, and the cache
get/put methods to locate where to apply the change.

In `@src/main/java/org/example/http/HttpResponseBuilder.java`:
- Around line 46-57: The buildHeaders() method always appends "Content-Length"
and "Connection: close" after iterating the headers map, causing duplicate
headers if callers put those keys into the headers map; update buildHeaders() to
filter out/ignore "Content-Length" and "Connection" (case-insensitively) when
iterating headers (or alternatively remove them from the headers map earlier,
e.g., in setHeaders), so only the canonical values appended by buildHeaders()
appear; ensure you reference the headers map and REASON_PHRASES/statusCode logic
in buildHeaders(), and adjust or document behavior used by callers like
StaticFileHandler to avoid duplicates.
- Around line 60-68: The method build() currently declares a local int
contentLength that shadows the instance field contentLength; remove the local
variable and assign the computed length directly to the instance field
(this.contentLength) so both code paths update the same field. In build(), when
body is empty and bytebody != null setBody(new String(bytebody,
StandardCharsets.UTF_8)) and then assign this.contentLength = bytebody.length;
otherwise compute this.contentLength =
body.getBytes(StandardCharsets.UTF_8).length (promote to long if needed) so the
class-level contentLength consistently reflects the computed value; adjust any
downstream code expecting a long accordingly.

In `@src/main/java/org/example/StaticFileHandler.java`:
- Around line 122-131: The lambda passed to cacheFilter.getOrFetch in
sendSmallFile currently ignores its parameter and closes over the outer file,
causing the provider to always read the same File; change the provider so it
actually uses its argument (the FileProvider.fetch(String path) parameter) to
read bytes (e.g., use Files.readAllBytes(Paths.get(path)) or new
File(path).toPath()) and ensure the key you pass (uri) matches the path
semantics expected by getOrFetch so the cached entry corresponds to the provided
path; update sendSmallFile, cacheFilter.getOrFetch call, and the lambda to
reference the path parameter instead of the captured file.
- Around line 97-120: Replace the long if-else in
StaticFileHandler.getContentType with a static Map<String,String> (e.g.
EXTENSION_CONTENT_TYPE) that maps extensions like "html","css","js","png", etc.
to their MIME types; initialize the map in a static block or as an immutable
map, then in getContentType extract the file extension from the uri, look it up
in EXTENSION_CONTENT_TYPE and return the mapped value if present; if not found,
call Files.probeContentType(path) as a fallback (catching IOExceptions) and
finally default to "application/octet-stream".

In `@src/test/java/org/example/FileCacheTest.java`:
- Around line 7-117: Add a new unit test that exercises FileCache's LRU eviction
by inserting enough entries to exceed MAX_CACHE_SIZE (and/or MAX_ENTRIES) and
asserting older keys are evicted and getCurrentSizeInBytes() stays <=
MAX_CACHE_SIZE; specifically target the eviction path exercised by
FileCache.removeEldestEntry and verify both contains(key) and get(key) for
evicted vs retained keys and that getCurrentSizeInBytes() does not drift upward
after multiple evictions to catch the bug in currentSize accounting.

In `@src/test/java/org/example/StaticFileHandlerTest.java`:
- Around line 206-244: Update the two tests to actually verify cache behavior
rather than just 200 responses: in testSmallFileUsesCaching, replace or wrap the
real CacheFilter with a mock/spy (or inject a test double) and assert that the
cache's fetch/store methods (e.g., CacheFilter.fetch or CacheFilter.get/put) are
called only once across the two handler.sendGetRequest calls for "cached.html";
alternatively, after the first successful request delete the underlying file and
assert the second request still returns 200 to prove the response came from
cache. In testLargeFileNotCached, similarly use a mock/spy CacheFilter and
assert that fetch/get is invoked for every handler.sendGetRequest call (i.e.,
called twice), or keep the file and verify that removing the file before the
second request causes the second request to fail if not cached (to prove no
caching). Ensure the tests reference handler.sendGetRequest and the CacheFilter
methods used by your server to locate instrumentation points.
- Around line 148-171: The test testStreamingFilesDoNotExceedMemoryThreshold is
flaky because it measures JVM heap via Runtime and uses ByteArrayOutputStream
which buffers the whole payload; replace the memory-based assertion with a
deterministic check: use a non-buffering counting OutputStream (implement a
simple OutputStream that increments a long counter on write) instead of
ByteArrayOutputStream, call handler.sendGetRequest(output, "memory_test.bin"),
assert the counter equals the expected file length and remove the Runtime memory
measurements and the 50MB assertion; reference the test method name
testStreamingFilesDoNotExceedMemoryThreshold and handler.sendGetRequest when
making the change.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7652687 and 1c57ca3.

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

@Martin-E-Karlsson Martin-E-Karlsson self-requested a review February 25, 2026 12:48
MartinStenhagen and others added 18 commits February 25, 2026 14:52
* Added basic YAML config-file.

* Added class ConfigLoader with static classes for encapsulation

* Added static metod loadOnce and step one of static method load

* Added static method createMapperFor that checks for YAML or JSON-files before creating an ObjectMapper object.

* implement ConfigLoader
refs #13

* Added AppConfig.java record for config after coderabbit feedback

* Updated ConfigLoader to use AppConfig record and jackson 3

* Added tests for ConfigLoader and reset cached method in ConfigLoader to ensure test isolation with static cache

* Removed unused dependency. Minor readability tweaks in AppConfig.

* Added check for illegal port numbers to withDefaultsApplied-method.

* Added test for illegal port numbers.
* Add 404 error handling to `StaticFileHandler` with fallback page

* Add test coverage for `StaticFileHandler` and improve constructor flexibility

- Introduced a new constructor in `StaticFileHandler` to support custom web root paths, improving testability.
- Added `StaticFileHandlerTest` to validate static file serving and error handling logic.

* Add test for 404 handling in `StaticFileHandler`

- Added a test to ensure nonexistent files return a 404 response.
- Utilized a temporary directory and fallback file for improved test isolation.

* Verify `Content-Type` header in `StaticFileHandlerTest` after running Pittest, mutations survived where setHeaders could be removed without test failure.

* Remove unused `.btn` styles from `pageNotFound.html`

* Improve 404 handling in `StaticFileHandler`: add fallback to plain text response if `pageNotFound.html` is missing, and fix typo in `pageNotFound.html`, after comments from CodeRabbit.
* Implements configuration loading and ensures that ConfigLoader is invoked during application startup (App.main).

* Minor formating.
* initial commit, added interfaces Filter and FilterChain

* Added HttpRequest class, groups together all information about a request that the server needs and easier to handle by future filters

* added interfaces Filter and FilterChain with Java Servlet Filter architecture.

* added FilterChainImpl

* Corrected imports from JDKS HttpRequest, to projects HttpRequest class

* Changed, params for FilterChain

* Updated HttpRequest with attributes,

* Revert "Updated HttpRequest with attributes,"

This reverts commit 0fd490e.
* Added MIME detector class and test class

* Added logic for Mime detector class

* Added Unit tests

* Added logic in HttpResponseBuilder and tests to try it out

* Solves duplicate header issue

* Removed a file for another issue

* Changed hashmap to Treemap per code rabbits suggestion

* Corrected logic error that was failing tests as per P2P review

* Added more reason phrases and unit testing, also applied code rabbits suggestions!

* Added changes to Responsebuilder to make merging easier

* Changed back to earlier commit to hande byte corruption new PR

* Added StaticFileHandler from main

* Added staticFileHandler with binary-safe writing

* Fix: Normalize Content-Type charset to uppercase UTF-8

Changed 'charset=utf-8' to 'charset=UTF-8' in StaticFileHandlerTest
to match MimeTypeDetector output and align with HTTP RFC standard.

Uppercase UTF-8 is the correct format per RFC 2616/7231.
* Update Dockerfile

Dockerfiles now copies www folder aswell

* Added building and copying of dependency jar files

* Fix dependency path in Dockerfile and update classpath in ENTRYPOINT configuration.

* Fixed typo in Entrypoint configuration

* Expose port 8080 in Dockerfile and changed appuser to come before ENTRYPOINT configuration.

* Adjust Dockerfile paths for classes and dependencies, update `COPY` targets accordingly.
* Added comprehensive README.MD

* Added formatting recommendations, clearer info
* Prevent path traversal and sanitize URI in StaticFileHandler.

* Add test for path traversal protection and support 403 responses.

* Add tests for URI sanitization and handling of encoded/special URLs.

* Add test for null byte injection prevention in StaticFileHandler

* Improve StaticFileHandler path traversal handling and test coverage

* Improve URI sanitization and add test for 404 response handling in StaticFileHandler
* Resolve port: CLI > config > default

* Wire port resolution to AppConfig/ConfigLoader and update docs/tests

* Update PortConfigurationGuide.md

* Update PortConfigurationGuide.md

* Remove ServerPortResolver; use ConfigLoader for port

* Update PortConfigurationGuide.md

* Update PortConfigurationGuide.md

* may be done
* refactor: add predefined HTTP status code constants to HttpResponseBuilder

* refactor: use status code constants in StaticFileHandler

* test: refactor StaticFileHandlerTest to use status code constants

* test: refactor HttpResponseBuilderTest to use status code constants and explicit assertations
* Fix path in Dockerfile for `www` directory copy operation

* Correct relative path for `www` directory in Dockerfile copy operation
* Add IpFilter and corresponding test skeleton

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Extend IpFilter with blocklist mode and add unit tests

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Enhance IpFilter to return 403 for blocked IPs and add corresponding test case

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Extend IpFilter with allowlist mode and add corresponding unit tests

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Refactor IpFilter to support both allowlist and blocklist modes, update logic, and add unit tests for allowlist mode

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Handle missing client IP in IpFilter, return 400 response, and add corresponding test case

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Refactor tests in `IpFilterTest` to use `assertAll` for improved assertion grouping and readability

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Refactor `IpFilter` to use `HttpResponse` instead of `HttpResponseBuilder` and update tests accordingly

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Add unit tests for edge cases in `IpFilter` allowlist and blocklist modes

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Refactor `IpFilter` and tests to use `HttpResponseBuilder` instead of `HttpResponse`

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Handle empty client IP in `IpFilter`, return 400 response, and add corresponding test case

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Add comments to `IpFilter` empty methods, clarifying no action is required

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Fix typos in comments within `IpFilterTest`

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Add Javadoc comments to `IpFilter` and `IpFilterTest` for improved clarity and documentation

* Refactor `IpFilter` to use thread-safe collections and normalize IP addresses

* Make `mode` field in `IpFilter` volatile to ensure thread safety

* Ensure UTF-8 encoding for response string in `IpFilterTest` and add attribute management to `HttpRequest`

* Ensure UTF-8 encoding for response string in `IpFilterTest` and add attribute management to `HttpRequest`

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Integrate IP filtering into `ConnectionHandler` and update configuration to support filter settings

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Refactor IP filter check in `ConnectionHandler` to use `Boolean.TRUE.equals` for improved null safety

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* Validate null inputs for allowed/blocked IPs in `IpFilter`, enhance test coverage, and fix typographical error in comments

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

* refactor: extract applyFilters() method using FilterChainImpl

Co-authored-by: Andreas Kaiberger <andreas.kaiberger@gmail.com>

* refactor: cache filter list at construction time

Co-authored-by: Andreas Kaiberger <andreas.kaiberger@gmail.com>

* refactor: cache filter list at construction time

Co-authored-by: Andreas Kaiberger <andreas.kaiberger@gmail.com>

* test: verify GPG signing

* Replace hardcoded status codes in `IpFilter` and `ConnectionHandler` with constants from `HttpResponseBuilder` for better maintainability

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>

---------

Co-authored-by: Rickard Ankar <rickard.ankar@iths.se>
* Re-commit LocaleFilter + tests to clean branch for PR

* Update LocaleFilter to handle quality weights and improve javadoc

* Fix: rename test method to reflect actual headers scenario

* Fix: ensure resolveLocale never returns empty string; strip quality weights
…re validering och beroendeinjektion i StaticFileHandler, samt uppdaterat hantering i CacheFilter
…soptimerad hantering av små filer, förbättrad URI-säkerhet, samt lagt till omfattande tester
…ache-hantering i FileCache med evictions och samtidighet, uppdaterad StaticFileHandler med bättre säkerhet, logik för streaming av stora filer, samt förbättrade tester
@EdvinSandgren EdvinSandgren self-requested a review February 26, 2026 09:43
@eeebbaandersson eeebbaandersson linked an issue Feb 26, 2026 that may be closed by this pull request
@AntonAhlqvist AntonAhlqvist self-requested a review February 26, 2026 10:33
@AntonAhlqvist
Copy link

Smart användning av read/write-locks, bra jobbat!

Copy link

@Martin-E-Karlsson Martin-E-Karlsson left a comment

Choose a reason for hiding this comment

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

Riktigt bra jobbat, det blev verkligen ett otroligt stort job du tog på dig! Toppen att du tog dig igenom alla merge issues dessutom! All funktionalitet verkar vara på plats och testerna av bade FelacheTest och de tillagda StaticFileHandler ser ut att vara täckande, väl implementerade och fungerar vid körning efter temporär merge med main.

Jag har framför allt nitpicks om magic numbers, språkanvänding och dokumentation. Vi har så vitt inte jag vet bestämt om vi ska hålla oss till Engelska eller inte i koden. Men jag tror att det är bra om exception meddelanden och annan kommunikation mot användaren är på engelska. Nitpicks:
HttpResponseBuilder:

  • Saknar Docstring
    CacheFilter
  • 10-11: Svenska exception meddelanden
  • Saknar Docstrings
    Filecache:
  • Docstrings och kommentarer skrivna på Svenska
  • 25: Initialisera Capacity och Load Factor som variable?
  • 34: Kvarglömd minneskommentar
  • 48, 59, 60: Svenska exception meddelanden
  • 67, 68, 107: Borde 1024 värdena lagras i en KILOBYTE_SIZE variable?
    StaticFileHandler:
  • Docstrings saknas på samtliga metoder
    StaticFileHandlerTest:
  • 169-170: Docstring på svenska
  • 111, 113, 132, 134, 153, 155: Värden som troligtvis borde lagras i beskrivande variable.

Som sagt jag har bara nitpicks, all funktionalitet ser bra ut!

Copy link

@EdvinSandgren EdvinSandgren left a comment

Choose a reason for hiding this comment

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

Tycker att det ser mycket bra ut! Bra jobbat.

@Kathify Kathify self-requested a review February 28, 2026 10:33
@Kathify
Copy link

Kathify commented Feb 28, 2026

tycker också det ser bra ut, snyggt jobbat!

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.

Implement file streaming for large files