Skip to content

Commit ad74e89

Browse files
Feature/429 page redirect (#126)
* Update RateLimitingFilter to return HTML response for rate-limiting errors and adjust related tests * Ignore macOS .DS_Store files * Show 429 message automatically when rate limit is exceeded, redirect client to start page when bucket is filled Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com> * Add Cache-Control: no-store to the 429 response, remove handle /rate-limited redirect in metric.js Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com> --------- Co-authored-by: Mattias Hagström <mattiashagstrommusic@gmail.com>
1 parent 280c9ee commit ad74e89

4 files changed

Lines changed: 56 additions & 10 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
target/
22
/.idea/
33
/META-INF
4+
5+
.DS_Store

src/main/java/org/juv25d/filter/RateLimitingFilter.java

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public class RateLimitingFilter implements Filter {
3636
* Constructs a new RateLimitingFilter.
3737
*
3838
* @param requestsPerMinute the number of requests allowed per minute for each IP
39-
* @param burstCapacity the maximum number of requests that can be handled in a burst
39+
* @param burstCapacity the maximum number of requests that can be handled in a burst
4040
* @throws IllegalArgumentException if requestsPerMinute or burstCapacity is not positive
4141
*/
4242
public RateLimitingFilter(long requestsPerMinute, long burstCapacity) {
@@ -81,7 +81,7 @@ public RateLimitingFilter() {
8181
}
8282

8383
/**
84-
* Applies the rate limiting logic to the incoming request.
84+
* Applies the rate-limiting logic to the incoming request.
8585
* If the rate limit is exceeded, a 429 Too Many Requests response is sent.
8686
*
8787
* @param req the HTTP request
@@ -91,7 +91,10 @@ public RateLimitingFilter() {
9191
*/
9292
@Override
9393
public void doFilter(HttpRequest req, HttpResponse res, FilterChain chain) throws IOException {
94-
if (!enabled) {chain.doFilter(req, res);return;}
94+
if (!enabled) {
95+
chain.doFilter(req, res);
96+
return;
97+
}
9598

9699
String clientIp = getClientIp(req);
97100

@@ -101,7 +104,7 @@ public void doFilter(HttpRequest req, HttpResponse res, FilterChain chain) throw
101104
chain.doFilter(req, res);
102105
} else {
103106
logRateLimitExceeded(clientIp, req.method(), req.path());
104-
sendTooManyRequests(res, clientIp);
107+
sendTooManyRequests(res);
105108
}
106109
}
107110

@@ -135,15 +138,52 @@ private void logRateLimitExceeded(String ip, String method, String path) {
135138
));
136139
}
137140

138-
private void sendTooManyRequests(HttpResponse res, String ip) {
139-
byte[] body = ("429 Too Many Requests: Rate limit exceeded for IP " + ip + "\n")
140-
.getBytes(StandardCharsets.UTF_8);
141+
private void sendTooManyRequests(HttpResponse res) {
142+
String html = """
143+
<!DOCTYPE html>
144+
<html>
145+
<head>
146+
<meta charset="UTF-8">
147+
<title>429 Too Many Requests</title>
148+
<style>
149+
body {
150+
font-family: Arial, sans-serif;
151+
max-width: 600px;
152+
margin: 100px auto;
153+
text-align: center;
154+
}
155+
h1 { color: #e74c3c; }
156+
p { color: #666; }
157+
</style>
158+
</head>
159+
<body>
160+
<h1>429 - Too Many Requests</h1>
161+
<p>You've exceeded the rate limit. Please wait a moment and try again.</p>
162+
<p>You will be able to retry in <span id="countdown">60</span> seconds.</p>
163+
<script>
164+
let seconds = 60;
165+
const el = document.getElementById('countdown');
166+
const timer = setInterval(() => {
167+
seconds--;
168+
el.textContent = seconds;
169+
if (seconds <= 0) {
170+
clearInterval(timer);
171+
window.location.href = '/';
172+
}
173+
}, 1000);
174+
</script>
175+
</body>
176+
</html>
177+
""";
178+
179+
byte[] body = html.getBytes(StandardCharsets.UTF_8);
141180

142181
res.setStatusCode(429);
143182
res.setStatusText("Too Many Requests");
144-
res.setHeader("Content-Type", "text/plain; charset=utf-8");
183+
res.setHeader("Content-Type", "text/html; charset=utf-8");
145184
res.setHeader("Content-Length", String.valueOf(body.length));
146185
res.setHeader("Retry-After", "60");
186+
res.setHeader("Cache-Control", "no-store");
147187
res.setBody(body);
148188
}
149189

src/main/resources/static/js/metric.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
async function loadHealth() {
22
try {
33
const res = await fetch("/metric");
4+
if (res.status === 429) {
5+
globalThis.location.href = "/";
6+
return;
7+
}
48
if (!res.ok) {
59
throw new Error(`HTTP ${res.status}`)
6-
};
10+
}
711
const data = await res.json();
812

913
document.getElementById("local-time").textContent = data.localTime;

src/test/java/org/juv25d/filter/RateLimitingFilterTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ void shouldBlockRequest_whenExceedingRateLimit() throws IOException {
6363
verifyNoMoreInteractions(chain);
6464
verify(res).setStatusCode(429);
6565
verify(res).setStatusText("Too Many Requests");
66-
verify(res).setHeader("Content-Type", "text/plain; charset=utf-8");
66+
verify(res).setHeader("Content-Type", "text/html; charset=utf-8");
6767
verify(res).setHeader(eq("Content-Length"), any());
6868
verify(res).setHeader("Retry-After", "60");
6969
verify(res).setBody(any());

0 commit comments

Comments
 (0)