Skip to content

fix: handle RateLimitBlockedException in ExceptionHandlers - #3472

Merged
JanCizmar merged 2 commits into
mainfrom
dkrizan/fix-rate-limit-blocked-exception-handler
Feb 18, 2026
Merged

fix: handle RateLimitBlockedException in ExceptionHandlers#3472
JanCizmar merged 2 commits into
mainfrom
dkrizan/fix-rate-limit-blocked-exception-handler

Conversation

@dkrizan

@dkrizan dkrizan commented Feb 17, 2026

Copy link
Copy Markdown
Member

Summary

  • Add missing @ExceptionHandler for RateLimitBlockedException in ExceptionHandlers.kt
  • Returns HTTP 444 (no body), matching the existing behavior in GlobalIpRateLimitFilter and GlobalUserRateLimitFilter

Problem

The strike-based rate limiting (#3451, commit 68b5250) introduced RateLimitBlockedException but only caught it in the global servlet filters. When thrown from controller-level rate limiting (e.g. the export endpoint's checkPerUserRateLimit), it bubbled up as an unhandled exception — causing HTTP 500 errors and Sentry noise since Feb 6th.

Test plan

  • Verify export endpoint returns 444 instead of 500 for repeat rate limit offenders
  • Confirm no new Sentry errors for RateLimitBlockedException

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of blocked rate-limit events so users receive a clear status when limits are exceeded and related occurrences are logged for better observability.
  • Tests

    • Made NDJSON error extraction in tests more resilient by scanning for error messages instead of relying on a fixed position.

The strike-based rate limiting (#3451) introduced RateLimitBlockedException
but only handled it in the global filters. When thrown from controller-level
rate limiting (e.g. export endpoint), it was unhandled, resulting in HTTP 500
errors reported to Sentry.

Add an @ExceptionHandler that returns HTTP 444 with no body, matching the
existing behavior in GlobalIpRateLimitFilter and GlobalUserRateLimitFilter.
@coderabbitai

coderabbitai Bot commented Feb 17, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds a global exception handler for RateLimitBlockedException in the Kotlin backend and adjusts a test to robustly extract an error message from NDJSON output.

Changes

Cohort / File(s) Summary
Rate limit exception handler
backend/app/src/main/kotlin/io/tolgee/ExceptionHandlers.kt
Added import for io.tolgee.security.ratelimit.RateLimitBlockedException and new @ExceptionHandler(RateLimitBlockedException::class) method handleRateLimitBlocked that logs strike count and returns HTTP 444 with an empty body.
Test: NDJSON error extraction
ee/backend/tests/src/test/kotlin/io/tolgee/ee/selfHostedLimitsAndReporting/CreditLimitTest.kt
Replaced fixed-index access to parsed[1]["errorMessage"] with a search over subsequent NDJSON items to collect and use the first non-null errorMessage, making assertion resilient to item order.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Suggested reviewers

  • JanCizmar

Poem

🐇 I hop through logs where handlers dwell,
Counting strikes with a tiny bell.
A quiet 444 I softly send,
Pausing the flurry until the end.
Hop, hop — the code will mend.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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 clearly and specifically describes the main change: adding exception handler for RateLimitBlockedException in ExceptionHandlers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dkrizan/fix-rate-limit-blocked-exception-handler

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.

@dkrizan
dkrizan requested a review from JanCizmar February 17, 2026 12:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
backend/app/src/main/kotlin/io/tolgee/ExceptionHandlers.kt (1)

260-264: Extract the magic status 444 to a shared constant and add explanatory comment.

The handler correctly mirrors the filter behavior (HTTP 444, no body). However, this non-standard status code is already used in GlobalIpRateLimitFilter (line 47), GlobalUserRateLimitFilter (line 45), and test expectations—all hardcoded as 444. Define a shared constant (e.g., const val RATE_LIMIT_BLOCKED_STATUS = 444) to keep them synchronized. Additionally, the filters both include explanatory comments about why 444 is used (nginx "No Response" to save bandwidth), but the exception handler lacks this context. Add the same clarifying comment here.

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

In `@backend/app/src/main/kotlin/io/tolgee/ExceptionHandlers.kt` around lines 260
- 264, Extract the magic number 444 into a shared constant (e.g., const val
RATE_LIMIT_BLOCKED_STATUS = 444) placed in a common accessible location used by
GlobalIpRateLimitFilter, GlobalUserRateLimitFilter and tests, then replace the
hardcoded 444 in the handleRateLimitBlocked function (and any other occurrences)
to use RATE_LIMIT_BLOCKED_STATUS; also add the same explanatory comment used in
the filters (explaining nginx "No Response" / saving bandwidth) immediately
above the handler so the rationale is documented next to handleRateLimitBlocked
and kept consistent across the filters and tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@backend/app/src/main/kotlin/io/tolgee/ExceptionHandlers.kt`:
- Around line 260-264: Extract the magic number 444 into a shared constant
(e.g., const val RATE_LIMIT_BLOCKED_STATUS = 444) placed in a common accessible
location used by GlobalIpRateLimitFilter, GlobalUserRateLimitFilter and tests,
then replace the hardcoded 444 in the handleRateLimitBlocked function (and any
other occurrences) to use RATE_LIMIT_BLOCKED_STATUS; also add the same
explanatory comment used in the filters (explaining nginx "No Response" / saving
bandwidth) immediately above the handler so the rationale is documented next to
handleRateLimitBlocked and kept consistent across the filters and tests.

JanCizmar
JanCizmar previously approved these changes Feb 17, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/selfHostedLimitsAndReporting/CreditLimitTest.kt (1)

110-112: firstOrNull() produces a misleading failure when no errorMessage chunk is present.

If the NDJSON response contains no chunk with an "errorMessage" key, errorMessage is null and the assertion on line 112 reports "expected <errorCode> but was <null>" — hiding the real problem (missing field) behind a value mismatch. Use first() so the failure immediately says "no such element" at the extraction step, not at the equality check.

♻️ Proposed fix
-    val errorMessage =
-      parsed.drop(1).filterIsInstance<Map<*, *>>().mapNotNull { it["errorMessage"] }.firstOrNull()
-    errorMessage.assert.isEqualTo(errorCode)
+    val errorMessage =
+      parsed.drop(1).filterIsInstance<Map<*, *>>().mapNotNull { it["errorMessage"] }.first()
+    errorMessage.assert.isEqualTo(errorCode)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ee/backend/tests/src/test/kotlin/io/tolgee/ee/selfHostedLimitsAndReporting/CreditLimitTest.kt`
around lines 110 - 112, The extraction of the errorMessage currently uses
firstOrNull(), which masks missing error chunks by returning null and causing a
misleading equality failure in errorMessage.assert.isEqualTo(errorCode); change
the terminal call to first() on the pipeline
parsed.drop(1).filterIsInstance<Map<*, *>>().mapNotNull { it["errorMessage"] }
so that a missing "errorMessage" immediately throws "no such element" during
extraction and surfaces the real issue.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@ee/backend/tests/src/test/kotlin/io/tolgee/ee/selfHostedLimitsAndReporting/CreditLimitTest.kt`:
- Around line 110-112: The extraction of the errorMessage currently uses
firstOrNull(), which masks missing error chunks by returning null and causing a
misleading equality failure in errorMessage.assert.isEqualTo(errorCode); change
the terminal call to first() on the pipeline
parsed.drop(1).filterIsInstance<Map<*, *>>().mapNotNull { it["errorMessage"] }
so that a missing "errorMessage" immediately throws "no such element" during
extraction and surfaces the real issue.

@dkrizan
dkrizan force-pushed the dkrizan/fix-rate-limit-blocked-exception-handler branch from 0404abe to 2d4e4fb Compare February 18, 2026 12:31

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
ee/backend/tests/src/test/kotlin/io/tolgee/ee/selfHostedLimitsAndReporting/CreditLimitTest.kt (1)

110-116: Consider guarding against null before asserting.

firstOrNull() makes errorMessage nullable. If no Map item carrying "errorMessage" is found, the assertion at line 116 will fail with Expected: null to be equal to: <errorCode>, which conflates "key absent" with "wrong value". A simple non-null check would produce a clearer failure signal:

♻️ Proposed improvement
-    val errorMessage =
-      parsed
-        .drop(1)
-        .filterIsInstance<Map<*, *>>()
-        .mapNotNull { it["errorMessage"] }
-        .firstOrNull()
-    errorMessage.assert.isEqualTo(errorCode)
+    val errorMessage =
+      parsed
+        .drop(1)
+        .filterIsInstance<Map<*, *>>()
+        .mapNotNull { it["errorMessage"] }
+        .firstOrNull()
+    checkNotNull(errorMessage) { "No errorMessage found in NDJSON items" }
+    errorMessage.assert.isEqualTo(errorCode)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@ee/backend/tests/src/test/kotlin/io/tolgee/ee/selfHostedLimitsAndReporting/CreditLimitTest.kt`
around lines 110 - 116, The extracted errorMessage from
parsed.drop(1)...firstOrNull() is nullable, so before calling
errorMessage.assert.isEqualTo(errorCode) add a null-guard to fail with a clear
message if the key is missing; locate the snippet in CreditLimitTest.kt where
variable errorMessage is computed and either assertNotNull(errorMessage) (or
throw a descriptive assertion failure like "expected errorMessage present but
was null") and then compare its value to errorCode to avoid conflating "missing
key" with "wrong value".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In
`@ee/backend/tests/src/test/kotlin/io/tolgee/ee/selfHostedLimitsAndReporting/CreditLimitTest.kt`:
- Around line 110-116: The extracted errorMessage from
parsed.drop(1)...firstOrNull() is nullable, so before calling
errorMessage.assert.isEqualTo(errorCode) add a null-guard to fail with a clear
message if the key is missing; locate the snippet in CreditLimitTest.kt where
variable errorMessage is computed and either assertNotNull(errorMessage) (or
throw a descriptive assertion failure like "expected errorMessage present but
was null") and then compare its value to errorCode to avoid conflating "missing
key" with "wrong value".

@dkrizan
dkrizan requested a review from JanCizmar February 18, 2026 13:07
@JanCizmar
JanCizmar merged commit c91aaef into main Feb 18, 2026
41 checks passed
@JanCizmar
JanCizmar deleted the dkrizan/fix-rate-limit-blocked-exception-handler branch February 18, 2026 17:09
TolgeeMachine added a commit that referenced this pull request Feb 18, 2026
## [3.159.1](v3.159.0...v3.159.1) (2026-02-18)

### Bug Fixes

* handle RateLimitBlockedException in ExceptionHandlers ([#3472](#3472)) ([c91aaef](c91aaef)), closes [#3451](#3451)
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.

2 participants