Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
843b34c
initial commit
JohanHiths Feb 16, 2026
2844d6e
Add `setNoCache` method to configure no-cache headers
JohanHiths Feb 17, 2026
0048a05
Improve Javadocs for `HttpCachingHeaders` and `HttpCachingHeadersTest`
JohanHiths Feb 18, 2026
e422463
Fix `addETagHeader` with better formatting after coderabbits suggesti…
JohanHiths Feb 18, 2026
4b3267d
Refactor `addETagHeader` for RFC compliance; update Javadocs and simp…
JohanHiths Feb 18, 2026
907e588
Add `CachingFilter` to enable HTTP caching with ETag and Last-Modifie…
JohanHiths Feb 21, 2026
ec9b987
Handle invalid `If-Modified-Since` parsing in `CachingFilter`; add ET…
JohanHiths Feb 22, 2026
b2ad6ee
Fixing
JohanHiths Feb 23, 2026
5ea159e
Fixing
JohanHiths Feb 23, 2026
af7fcdb
Fix
JohanHiths Feb 23, 2026
c27a17d
restore deleted files
JohanHiths Feb 23, 2026
7bbcdd4
Coderabbit review
JohanHiths Feb 23, 2026
5bd97d0
Update `CachingFilter` to include ETag in response headers; add `addH…
JohanHiths Feb 23, 2026
90b6fc4
Add unit tests for `CachingFilter` functionality; improve path normal…
JohanHiths Feb 24, 2026
5962ecb
Add unit tests for `CachingFilter` functionality; improve path normal…
JohanHiths Feb 24, 2026
456cda3
Add unit tests for CachingFilter functionality; improve path normaliz…
JohanHiths Feb 24, 2026
c1321dd
Add unit tests for `CachingFilter` functionality; improve path normal…
JohanHiths Feb 24, 2026
5552e85
Add unit tests for CachingFilter functionality; improve path normaliz…
JohanHiths Feb 24, 2026
a9cffb6
Merge branch 'main' into http-caching-headers
JohanHiths Feb 24, 2026
cbb0743
Remove getStatusCode method from HttpResponseBuilder
JohanHiths Feb 24, 2026
d121ec8
Update `CachingFilter` to enhance caching logic, centralize header ha…
JohanHiths Feb 26, 2026
e39dad8
Refactor to improve module structure:
JohanHiths Feb 27, 2026
7d8b245
Merge branch 'main' into http-caching-headers
JohanHiths Mar 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/main/java/org/example/ResolveFileHandler.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.example;

public class ResolveFileHandler {


public void handleCaching(){

}
}
96 changes: 96 additions & 0 deletions src/main/java/org/example/filter/CachingFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.example.filter;

import org.example.config.ConfigLoader;
import org.example.http.HttpCachingHeaders;
import org.example.http.HttpResponseBuilder;
import org.example.httpparser.HttpRequest;

import java.io.File;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Map;


public class CachingFilter implements Filter {


@Override
public void init() {

}

@Override
public void destroy() {

}

@Override
public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) {

String path = request.getPath();
HttpCachingHeaders cachingHeaders = new HttpCachingHeaders();

if (path.equals("/")) {
path = "index.html";
} else {
path = path.substring(1);
}

// Ingen mer hårtkodat utan sökväg från ConfigLoader
String rootDir = ConfigLoader.get().server().rootDir();
File file = new File(rootDir, path);


if(!file.exists()){
response.setStatusCode(HttpResponseBuilder.SC_NOT_FOUND);

return;
}

Map<String, String> headers = request.getHeaders();

String modifiedSince = headers.get("If-Modified-Since");
String eTag = generateEtag(file);
Instant lastModified = Instant.ofEpochMilli(file.lastModified());

String ifNoneMatch = headers.get("If-None-Match");

cachingHeaders.addETagHeader(eTag);
cachingHeaders.setLastModified(Instant.ofEpochMilli(file.lastModified()));
cachingHeaders.setDefaultCacheControlStatic();


if (ifNoneMatch != null && ifNoneMatch.equals(eTag)) {
response.setStatusCode(HttpResponseBuilder.SC_NOT_MODIFIED);
cachingHeaders.getHeaders().forEach(response::addHeader);
return;
}

if (modifiedSince != null) {
try {
Instant ifModifiedSinceInstant =
Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(modifiedSince));

if (!lastModified.isAfter(ifModifiedSinceInstant)) {
response.setStatusCode(HttpResponseBuilder.SC_NOT_MODIFIED);
cachingHeaders.getHeaders().forEach(response::addHeader);
return;
}

} catch (Exception e) {

}
}

chain.doFilter(request, response);
cachingHeaders.getHeaders().forEach(response::addHeader);


}

private String generateEtag(File file) {
return "\"" + file.lastModified() + "-" + file.length() + "\"";

}
}

108 changes: 108 additions & 0 deletions src/main/java/org/example/http/HttpCachingHeaders.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.example.http;

import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
//
/**
* Helper class for building HTTP response headers
* Lets the client reuse cached responses
* Reduces bandwidth
* Reduces latency
* Reduces load on your server
* Ensures webserver and proxies understand caching instructions
*/
public class HttpCachingHeaders {

/**
* Cache Control helps manage servers and browsers by settings rules
* ETag helps cache be more efficient and not needing to send a full resend assuming the content has not changed
* Last-Modified
*/

private static final String CACHE_CONTROL = "Cache-Control";
private static final String LAST_MODIFIED = "Last-Modified";
private static final String ETAG = "ETag";

private static final DateTimeFormatter HTTP_DATE_FORMATTER =
DateTimeFormatter.RFC_1123_DATE_TIME.withZone(ZoneOffset.UTC);


private final Map<String, String> headers = new LinkedHashMap<>();


/**
* Sets a header
* @param name Header name eg. Cache-Control
* @param value Header value eg. public, max-age=3600
*/
public void setHeader(String name, String value) {
headers.put(name, value);
}

/**
* Helper method for setting ETag header value
* ETag values must be enclosed in double quotes "123" not 123
* @param etag Raw, unquoted ETag token (e.g. {`@code` abc123}); double quotes
* are added automatically to comply with RFC 7232.
*/
public void addETagHeader(String etag) {
setHeader(ETAG, etag);
}
Comment thread
JohanHiths marked this conversation as resolved.
Comment thread
JohanHiths marked this conversation as resolved.

/**
* Sets Cache-Control header value
* @param cacheControl sets rules eg. public, max-age=3600
*/
public void setCacheControl(String cacheControl) {
setHeader(CACHE_CONTROL, cacheControl);
}

/**
* Helper method for setting Last-Modified header value
* Formates and sets Last modified based on an instant
* @param instant Timestamp of the last modification
*/
public void setLastModified(Instant instant){
setHeader(LAST_MODIFIED, HTTP_DATE_FORMATTER.format(instant));
}


/**
* In case of errors or unexpected behaviour, the cache should be disabled and no data should be saved
*/
public void setNoCache() {
setCacheControl("no-store, no-cache");
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.


/**
* Copies all configured caching headers into the provided target map eg. HttpReponseBuilder.
* @param target Map should return generated headers
*/
public void applyTo(Map<String,String> target){
target.putAll(headers);
}

/**
* Maps all configured caching headers into a new map
* @return A map which includes all caching headers
*/
public Map<String,String> getHeaders() {
return new LinkedHashMap<>(headers);
}



/**
* Standard settings for caching, 1 hour
*/
public void setDefaultCacheControlStatic(){
setCacheControl("public, max-age=3600");
}



}
8 changes: 8 additions & 0 deletions src/main/java/org/example/http/HttpResponseBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,13 @@ public void setContentTypeFromFilename(String filename) {
setHeader("Content-Type", mimeType);
}


public void addHeader(String key, String value){
this.headers.put(key, value);
}



/*
* Builds the complete HTTP response as a byte array and preserves binary content without corruption.
* @return Complete HTTP response (headers + body) as byte[]
Expand Down Expand Up @@ -150,6 +157,7 @@ public byte[] build() {
System.arraycopy(contentBody, 0, response, headerBytes.length, contentBody.length);

return response;

}

public Map<String, String> getHeaders() {
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/org/example/httpparser/HttpRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
import org.jspecify.annotations.Nullable;

import java.util.Collections;

import java.util.HashMap;

import java.util.Map;

/*
Expand All @@ -17,9 +19,13 @@ public class HttpRequest {
private final String path;
private final String version;
private final Map<String, String> headers;


private final @Nullable String body;

private final Map<String, Object> attributes = new HashMap<>();


public HttpRequest(String method,
String path,
String version,
Expand All @@ -42,10 +48,13 @@ public Map<String, String> getHeaders() {
return headers; }
public @Nullable String getBody() {
return body; }


public void setAttribute(String key, Object value) {
attributes.put(key, value);
}
public Object getAttribute(String key) {
return attributes.get(key);
}

}
76 changes: 76 additions & 0 deletions src/test/java/org/example/filter/CachingFilterTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.example.filter;

import org.example.config.ConfigLoader;
import org.example.http.HttpResponseBuilder;
import org.example.httpparser.HttpRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Map;
/// //
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

public class CachingFilterTest {


@BeforeEach
void setup() {
Path configPath = Paths.get("src/test/resources/test-config.yml");
ConfigLoader.loadOnce(configPath);

}

@Test
void shouldReturn404WhenFileDoesNotExist() {

CachingFilter cachingFilter = new CachingFilter();


HttpRequest request = new HttpRequest(
"GET",
"/does-not-exist",
"HTTP/1.1",
Map.of(),
null
);

HttpResponseBuilder response = new HttpResponseBuilder();

TestFilterChain chain = new TestFilterChain();

cachingFilter.doFilter(request, response, chain);

assertThat(response.getStatusCode()).isEqualTo(404);
assertThat(chain.called).isFalse();
}


@Test
void shouldContinueChainWhenNoCachingHeaders() throws Exception {

CachingFilter cachingFilter = new CachingFilter();

File file = new File("www/ok.txt");
file.getParentFile().mkdirs();
Files.writeString(file.toPath(), "hello");

HttpRequest request = new HttpRequest(
"GET",
"/ok.txt",
"HTTP/1.1",
Map.of(),
null
);

HttpResponseBuilder response = new HttpResponseBuilder();
TestFilterChain chain = new TestFilterChain();

cachingFilter.doFilter(request, response, chain);

assertThat(chain.called).isTrue();
}
}
13 changes: 13 additions & 0 deletions src/test/java/org/example/filter/TestFilterChain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.example.filter;

import org.example.http.HttpResponseBuilder;
import org.example.httpparser.HttpRequest;
/// /
class TestFilterChain implements FilterChain {
boolean called = false;

@Override
public void doFilter(HttpRequest request, HttpResponseBuilder response) {
called = true;
}
}
Loading