-
Notifications
You must be signed in to change notification settings - Fork 0
Http caching headers #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JohanHiths
wants to merge
23
commits into
main
Choose a base branch
from
http-caching-headers
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
843b34c
initial commit
JohanHiths 2844d6e
Add `setNoCache` method to configure no-cache headers
JohanHiths 0048a05
Improve Javadocs for `HttpCachingHeaders` and `HttpCachingHeadersTest`
JohanHiths e422463
Fix `addETagHeader` with better formatting after coderabbits suggesti…
JohanHiths 4b3267d
Refactor `addETagHeader` for RFC compliance; update Javadocs and simp…
JohanHiths 907e588
Add `CachingFilter` to enable HTTP caching with ETag and Last-Modifie…
JohanHiths ec9b987
Handle invalid `If-Modified-Since` parsing in `CachingFilter`; add ET…
JohanHiths b2ad6ee
Fixing
JohanHiths 5ea159e
Fixing
JohanHiths af7fcdb
Fix
JohanHiths c27a17d
restore deleted files
JohanHiths 7bbcdd4
Coderabbit review
JohanHiths 5bd97d0
Update `CachingFilter` to include ETag in response headers; add `addH…
JohanHiths 90b6fc4
Add unit tests for `CachingFilter` functionality; improve path normal…
JohanHiths 5962ecb
Add unit tests for `CachingFilter` functionality; improve path normal…
JohanHiths 456cda3
Add unit tests for CachingFilter functionality; improve path normaliz…
JohanHiths c1321dd
Add unit tests for `CachingFilter` functionality; improve path normal…
JohanHiths 5552e85
Add unit tests for CachingFilter functionality; improve path normaliz…
JohanHiths a9cffb6
Merge branch 'main' into http-caching-headers
JohanHiths cbb0743
Remove getStatusCode method from HttpResponseBuilder
JohanHiths d121ec8
Update `CachingFilter` to enhance caching logic, centralize header ha…
JohanHiths e39dad8
Refactor to improve module structure:
JohanHiths 7d8b245
Merge branch 'main' into http-caching-headers
JohanHiths File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| package org.example; | ||
|
|
||
| public class ResolveFileHandler { | ||
|
|
||
|
|
||
| public void handleCaching(){ | ||
|
|
||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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() + "\""; | ||
|
|
||
| } | ||
| } | ||
|
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
|
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"); | ||
| } | ||
|
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"); | ||
| } | ||
|
|
||
|
|
||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.