Skip to content

Add background scheduler for monitor checks - #1

Merged
omarmenew24-collab merged 6 commits into
mainfrom
feature/scheduler
Jun 21, 2026
Merged

Add background scheduler for monitor checks#1
omarmenew24-collab merged 6 commits into
mainfrom
feature/scheduler

Conversation

@omarmenew24-collab

@omarmenew24-collab omarmenew24-collab commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Implements the core monitoring engine: a cron job that runs every minute, checks all due monitors via HTTP GET with 5s timeout, logs results to check_logs, and updates monitor state (last_status, consecutive_failures, is_alerted) atomically within a transaction.

  • node-cron scheduler with overlap prevention
  • Transaction support added to db.js (withTransaction)
  • Checks service: runCheck, processCheck, checkAllDueMonitors
  • Concurrent checks via Promise.allSettled (one failure doesn't block others)
  • Alert flag set when consecutive_failures >= threshold
  • Recovery resets consecutive_failures and is_alerted

Summary by CodeRabbit

  • New Features
    • Added a cron-driven scheduler that periodically checks due monitors, logs results, updates monitor state, and cleans up check logs.
  • Security / Validation
    • Strengthened URL validation with hostname/IP safety checks and DNS-based blocking of unsafe destinations.
    • Added safe failure handling for blocked/invalid targets and request timeouts.
  • Bug Fixes
    • Prevents overlapping scheduler runs, throttles concurrent checks, and records check+state updates atomically while continuing after individual failures.
  • Documentation
    • Expanded scheduler and reliability guidance, including DNS rebinding/TOCTOU considerations.

Implements the core monitoring engine: a cron job that runs every minute,
checks all due monitors via HTTP GET with 5s timeout, logs results to
check_logs, and updates monitor state (last_status, consecutive_failures,
is_alerted) atomically within a transaction.

- node-cron scheduler with overlap prevention
- Transaction support added to db.js (withTransaction)
- Checks service: runCheck, processCheck, checkAllDueMonitors
- Concurrent checks via Promise.allSettled (one failure doesn't block others)
- Alert flag set when consecutive_failures >= threshold
- Recovery resets consecutive_failures and is_alerted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f1e9865d-705b-4ed2-b94f-b27323ec7a57

📥 Commits

Reviewing files that changed from the base of the PR and between d2d1b9b and 9a202e8.

⛔ Files ignored due to path filters (1)
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • backend/package.json
  • backend/src/config/db.js
  • backend/src/db/checks.queries.js
  • backend/src/db/retention.queries.js
  • backend/src/scheduler/index.js
  • backend/src/schemas/monitors.schema.js
  • backend/src/server.js
  • backend/src/services/checks.service.js
  • backend/src/utils/url-safety.js
  • context1/feature-specs/08-scheduler.md
  • context1/learning.md

📝 Walkthrough

Walkthrough

Adds a background monitor scheduler using node-cron that runs every minute to check due monitors, with a 30-day log retention job running daily. A withTransaction helper enables atomic DB updates. Three query functions handle due monitor selection, check log insertion, and state updates. A service layer performs SSRF-safe HTTP GET checks with 5s timeout, manages consecutive failure and alert flag transitions, and executes checks concurrently with a limit of 50. A cron job with an overlap guard is wired into server startup. URL safety validation blocks private IPs (including IPv6), localhost, and metadata endpoints.

Changes

Monitor Scheduler Pipeline

Layer / File(s) Summary
Transaction helper and DB query functions
backend/src/config/db.js, backend/src/db/checks.queries.js
withTransaction acquires a pool client, executes BEGIN/COMMIT/ROLLBACK, and always releases. findDueMonitors, insertCheckLog, and updateMonitorAfterCheck implement the three SQL operations for each check cycle.
SSRF protection via URL hostname validation
backend/src/utils/url-safety.js, backend/src/schemas/monitors.schema.js
isPrivateIP classifies private/reserved IPv4 and IPv6 addresses using ipaddr.js. validateUrlHostname rejects blocked hostnames (localhost, metadata.google.internal) and private IP literals. resolveAndValidate performs DNS lookup with multi-record support (all: true) and validates resolved IPs. Monitor schema integrates validateUrlHostname to block unsafe URLs at creation time.
HTTP check service and state transitions
backend/src/services/checks.service.js
runCheck performs SSRF-safe URL resolution, returns early for blocked URLs, then executes GET with 5s timeout, cancels the response body, and maps outcomes to up/down/timeout. processCheck writes the log and updates monitor state inside a transaction, tracking consecutive failures and alert flag transitions. checkAllDueMonitors fetches due monitors and processes them concurrently via p-limit(50) with Promise.allSettled.
Cron scheduler and log retention
backend/package.json, backend/src/scheduler/index.js, backend/src/db/retention.queries.js, backend/src/server.js
node-cron is added as a dependency. Per-minute cron job runs checkAllDueMonitors with an isRunning guard to prevent overlaps. Daily cron job at 03:00 runs deleteExpiredCheckLogs to retain check logs for 30 days. Scheduler is imported at server boot.
Feature specification and learning documentation
context1/feature-specs/08-scheduler.md, context1/learning.md
Adds the complete scheduler spec: code standards, dependency rationale, withTransaction contract, SQL interfaces, service-layer logic with SSRF resolution, HTTP check requirements, out-of-scope items, and acceptance criteria. Learning writeup documents five failure modes and fixes: unbounded concurrency (mitigated via p-limit), log growth (mitigated via retention), socket leak (mitigated via response.body?.cancel()), IPv6 SSRF bypass (mitigated via ipaddr.js with parse-failure safety), and DNS rebinding / TOCTOU (mitigated via multi-record lookup with acknowledged limitations).

Sequence Diagram(s)

sequenceDiagram
  participant Cron as node-cron
  participant Scheduler as scheduler/index.js
  participant Service as checks.service.js
  participant Safety as resolveAndValidate
  participant HTTP as fetch
  participant TX as withTransaction
  participant DB as PostgreSQL

  Cron->>Scheduler: tick every minute
  Scheduler->>Scheduler: check isRunning guard
  Scheduler->>Service: checkAllDueMonitors()
  Service->>DB: findDueMonitors() SELECT
  DB-->>Service: monitor rows

  loop each monitor (p-limit throttled, Promise.allSettled)
    Service->>Service: processCheck(monitor)
    Service->>Safety: resolveAndValidate(url)
    alt URL safe
      Safety-->>Service: {safe: true}
      Service->>HTTP: GET with AbortSignal.timeout(5000)
      HTTP-->>Service: response
      Service->>Service: cancel response.body
      Service->>TX: withTransaction(callback)
      TX->>DB: BEGIN
      TX->>DB: insertCheckLog(client, monitorId, checkResult)
      TX->>DB: updateMonitorAfterCheck(client, monitorId, updates)
      TX->>DB: COMMIT
    else URL unsafe
      Safety-->>Service: {safe: false, reason}
      Service-->>Service: return {status: down, message: Blocked...}
    end
  end

  Scheduler->>Scheduler: reset isRunning in finally
  
  Note over Cron,DB: Separate daily job at 03:00
  Cron->>Scheduler: tick at 03:00
  Scheduler->>DB: deleteExpiredCheckLogs()
  DB-->>Scheduler: deleted count
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Poem

🐇 Hop hop, the clock ticks every minute here,
A cron job wakes and checks if monitors are near,
No secret metadata! No loopback homes allowed,
With BEGIN and COMMIT, transactions wrap the crowd,
p-limit throttles the herd, while 30-day logs fade out loud. 🌱

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary change: introduction of a background scheduler for monitor checks, which is the core feature of this pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/scheduler

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
context1/feature-specs/08-scheduler.md (2)

35-37: 💤 Low value

Add language identifier to shell code block.

Markdown linting requires fenced code blocks to specify a language. Add sh or bash identifier.

Proposed fix
-```
+```sh
 cd backend && npm install node-cron
-```
+```
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@context1/feature-specs/08-scheduler.md` around lines 35 - 37, The fenced code
block containing the npm install command is missing a language identifier, which
violates markdown linting rules. Add `sh` as the language identifier immediately
after the opening triple backticks (before the newline) so the code block
properly specifies that it contains shell commands. This applies to the code
block in the Scheduler section that shows the cd backend && npm install
node-cron command.

Source: Linters/SAST tools


249-249: 💤 Low value

Use hyphen for compound adjective modifying "logic".

"failed DB writes" is a compound adjective that should be hyphenated when it directly precedes the noun. Consider: "Retry logic for failed-DB writes" or restructure to "Retry logic for DB write failures."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@context1/feature-specs/08-scheduler.md` at line 249, The line contains a
compound adjective that should be hyphenated when modifying a noun. Review the
phrase at line 249 and apply hyphenation to compound modifiers that precede
nouns, such as changing "Rate limiting checks" to "Rate-limiting checks" to
follow standard grammar conventions for compound adjectives. Apply this same
rule throughout the document wherever compound adjectives (like "failed DB
writes" becoming "failed-DB writes") directly precede the noun they modify.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/config/db.js`:
- Around line 19-21: The catch block for the transaction in the database
connection code can lose the original error if the ROLLBACK query itself fails.
Wrap the client.query('ROLLBACK') call in its own try-catch block so that if
ROLLBACK throws an error, it does not overwrite the original transaction error.
In the ROLLBACK catch block, log the rollback failure but then re-throw the
original error that was caught in the outer catch block, ensuring the root cause
of the transaction failure is not masked by any rollback failure.

In `@backend/src/db/checks.queries.js`:
- Around line 3-11: The findDueMonitors function performs a plain SELECT query
without row-level locking, allowing multiple concurrent app instances to read
and claim the same monitors for processing, which causes duplicate processing
and data integrity issues. Modify the function to use an atomic claim pattern by
replacing the SELECT with an UPDATE statement that uses FOR UPDATE SKIP LOCKED
to lock due monitors and atomically claim them (e.g., by updating a claim
timestamp or instance identifier), then RETURNING the claimed rows to ensure
each due monitor is exclusively processed by only one instance across all
running app servers.

In `@backend/src/services/checks.service.js`:
- Around line 87-89: The Promise.allSettled call in the checks service processes
all monitors from dueMonitors.map concurrently without any concurrency limit,
which can cause resource exhaustion. Implement concurrency limiting by using a
library like p-limit (or similar) to cap the number of simultaneous processCheck
calls, allowing only a reasonable number of monitors to be processed in parallel
at any given time. Apply the concurrency limiter to the dueMonitors.map call so
that the queue of processCheck promises is throttled rather than all being
created at once.
- Around line 10-14: Before the fetch operation in the checks.service.js block
containing the GET request with signal and redirect options, implement URL
validation to protect against SSRF attacks. Parse and validate the url parameter
to ensure it only uses http or https protocols and does not resolve to private
IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), localhost addresses
(127.0.0.1, ::1), link-local addresses (169.254.x.x), or metadata services
(169.254.169.254). Perform hostname resolution safety checks to verify the
resolved IP is not in a reserved range, and either change the redirect option
from 'follow' to 'manual' or validate that any redirect targets pass the same IP
range validation before following them. Throw an appropriate error if the URL
fails any validation check to prevent the fetch from executing.

In `@context1/feature-specs/08-scheduler.md`:
- Line 44: The backend specification assumes Node 24 for native fetch support,
but there is no explicit version constraint in the package.json or .nvmrc file
to enforce this requirement. Add an `engines` field to the backend's
package.json file specifying the minimum Node.js version as 24.0.0, or
alternatively create a .nvmrc file in the backend directory containing the Node
version. This ensures developers use the correct Node.js version and prevents
compatibility issues from using older versions.

---

Nitpick comments:
In `@context1/feature-specs/08-scheduler.md`:
- Around line 35-37: The fenced code block containing the npm install command is
missing a language identifier, which violates markdown linting rules. Add `sh`
as the language identifier immediately after the opening triple backticks
(before the newline) so the code block properly specifies that it contains shell
commands. This applies to the code block in the Scheduler section that shows the
cd backend && npm install node-cron command.
- Line 249: The line contains a compound adjective that should be hyphenated
when modifying a noun. Review the phrase at line 249 and apply hyphenation to
compound modifiers that precede nouns, such as changing "Rate limiting checks"
to "Rate-limiting checks" to follow standard grammar conventions for compound
adjectives. Apply this same rule throughout the document wherever compound
adjectives (like "failed DB writes" becoming "failed-DB writes") directly
precede the noun they modify.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d75601b9-4475-4c6c-9734-6d4cd089f435

📥 Commits

Reviewing files that changed from the base of the PR and between d2d1b9b and 69787fd.

⛔ Files ignored due to path filters (1)
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (7)
  • backend/package.json
  • backend/src/config/db.js
  • backend/src/db/checks.queries.js
  • backend/src/scheduler/index.js
  • backend/src/server.js
  • backend/src/services/checks.service.js
  • context1/feature-specs/08-scheduler.md

Comment thread backend/src/config/db.js
Comment on lines +3 to +11
export const findDueMonitors = async () => {
const result = await query(
`SELECT id, url, failure_threshold, consecutive_failures, is_alerted, interval_minutes
FROM monitors
WHERE next_check_at <= NOW()
AND is_active = true
AND is_deleted = false`
);
return result.rows;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Due monitor selection is not globally exclusive.

findDueMonitors() uses a plain read, so concurrent app instances can pick the same monitor in the same minute. Combined with snapshot-based counter math in processCheck (backend/src/services/checks.service.js Line 58-Line 69), this can double-insert logs and lose failure-count updates.

Please switch to an atomic “claim due monitors” pattern (e.g., FOR UPDATE SKIP LOCKED + UPDATE ... RETURNING) so each due monitor is processed once across instances.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/db/checks.queries.js` around lines 3 - 11, The findDueMonitors
function performs a plain SELECT query without row-level locking, allowing
multiple concurrent app instances to read and claim the same monitors for
processing, which causes duplicate processing and data integrity issues. Modify
the function to use an atomic claim pattern by replacing the SELECT with an
UPDATE statement that uses FOR UPDATE SKIP LOCKED to lock due monitors and
atomically claim them (e.g., by updating a claim timestamp or instance
identifier), then RETURNING the claimed rows to ensure each due monitor is
exclusively processed by only one instance across all running app servers.

Comment thread backend/src/services/checks.service.js
Comment thread backend/src/services/checks.service.js
- Standard for scheduled tasks in Node.js
- Easy to read: `'* * * * *'` = every minute

The backend is Node 24, which has native `fetch` — no HTTP client needed.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Node.js version target in project
echo "=== Checking Node version target ==="
[ -f backend/.nvmrc ] && echo ".nvmrc:" && cat backend/.nvmrc
[ -f backend/package.json ] && echo "package.json engines:" && jq '.engines' backend/package.json 2>/dev/null || echo "No engines field"
find backend -name .tool-versions -o -name .github/workflows/*.yml | head -5 | xargs grep -l "node\|Node" 2>/dev/null | head -3

Repository: omarmenew24-collab/Uptime-Monitoring

Length of output: 144


🏁 Script executed:

# Search more thoroughly for Node.js version specifications
find . -type f -name "*.yml" -o -name "*.yaml" | head -20

Repository: omarmenew24-collab/Uptime-Monitoring

Length of output: 62


🏁 Script executed:

# Check for Node version in root .nvmrc or other config files
ls -la | grep -E "nvmrc|tool-versions|volta"
cat .nvmrc 2>/dev/null || echo "No root .nvmrc"

Repository: omarmenew24-collab/Uptime-Monitoring

Length of output: 95


🌐 Web query:

Node.js 24 native fetch support availability

💡 Result:

Node.js 24 has native, global fetch support (it is not behind an experimental flag in modern Node 24 builds). In Node v24 documentation, fetch is described as a browser-compatible implementation, available as a global (along with Request/Response/Headers) [1][2]. The underlying implementation is powered by Undici [3]. Practical availability notes for Node.js 24 (as of today, 2026-06-21): 1) What you can use: In Node 24, fetch and related classes (Request, Response, Headers) are available from the global scope [1][4]. 2) How it’s implemented: Node’s fetch API is powered by Undici [3]. 3) No extra “experimental flag” needed: Node’s “Global objects” documentation for v24 states it is “No longer behind --experimental-fetch CLI flag” [1]. Confidence: high that Node.js 24 provides native global fetch support, based on official Node.js docs for v24 globals and the Node Learn/Undici materials [1][3].

Citations:


🏁 Script executed:

# More thorough search for CI/CD and configuration files
find . -path ./node_modules -prune -o -type f \( -name "*.yml" -o -name "*.yaml" -o -name ".github" -type d \) -print | head -30

Repository: omarmenew24-collab/Uptime-Monitoring

Length of output: 62


🏁 Script executed:

# Check if .github/workflows directory exists and what's inside
ls -la .github/workflows/ 2>/dev/null || echo "No .github/workflows directory"

Repository: omarmenew24-collab/Uptime-Monitoring

Length of output: 111


🏁 Script executed:

# Search for any mention of Node version in common config files
cat package.json | head -50

Repository: omarmenew24-collab/Uptime-Monitoring

Length of output: 125


🏁 Script executed:

# Check if backend/package.json exists and examine it more carefully
cat backend/package.json

Repository: omarmenew24-collab/Uptime-Monitoring

Length of output: 739


🏁 Script executed:

# Look at the spec file mentioned in the review
cat "context1/feature-specs/08-scheduler.md"

Repository: omarmenew24-collab/Uptime-Monitoring

Length of output: 9994


🏁 Script executed:

# Check directory structure to understand project layout
ls -la backend/ 2>/dev/null | head -20

Repository: omarmenew24-collab/Uptime-Monitoring

Length of output: 567


Add Node.js version constraint to match spec assumption.

The spec correctly states that Node 24 has native fetch. However, the backend's package.json lacks an engines field, and there is no .nvmrc or CI configuration to enforce Node 24. Developers could inadvertently use Node 18, 20, or 22. Add an explicit version constraint:

"engines": {
  "node": ">=24.0.0"
}

Or create backend/.nvmrc with 24 to document the target version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@context1/feature-specs/08-scheduler.md` at line 44, The backend specification
assumes Node 24 for native fetch support, but there is no explicit version
constraint in the package.json or .nvmrc file to enforce this requirement. Add
an `engines` field to the backend's package.json file specifying the minimum
Node.js version as 24.0.0, or alternatively create a .nvmrc file in the backend
directory containing the Node version. This ensures developers use the correct
Node.js version and prevents compatibility issues from using older versions.

omarmenew24-collab and others added 2 commits June 21, 2026 10:49
Wraps the ROLLBACK call in its own try/catch so a failed rollback
(e.g. dropped connection) doesn't swallow the original transaction error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two layers of defense against Server-Side Request Forgery:

1. Input validation (Zod schema): blocks private IPs, localhost,
   and reserved hostnames at monitor creation time
2. DNS resolution check (before fetch): resolves hostname and verifies
   the IP isn't private/reserved before the HTTP request is sent,
   catching cases where a public hostname resolves to a private IP

Blocked ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16,
127.0.0.0/8, 169.254.0.0/16, ::1, fc00::/7, fe80::/10

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/services/checks.service.js (1)

22-26: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

HTTP redirects bypass SSRF protection.

The redirect: 'follow' option allows the request to follow redirects without re-validating the target URL. An attacker could host a URL that passes validation, then redirects to http://169.254.169.254/ or other internal endpoints.

Change to redirect: 'manual' and either reject redirects or validate each redirect target.

🛡️ Proposed fix to prevent redirect-based SSRF
     const response = await fetch(url, {
       method: 'GET',
       signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
-      redirect: 'follow',
+      redirect: 'manual',
     });

     const responseTimeMs = Date.now() - startTime;

-    if (response.ok) {
+    // For monitors, a redirect (3xx) should still be considered "up"
+    // but we don't follow it to prevent SSRF
+    if (response.ok || (response.status >= 300 && response.status < 400)) {
       return {
         status: 'up',
         responseCode: response.status,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/services/checks.service.js` around lines 22 - 26, The fetch
request in the checks service uses redirect: 'follow' which bypasses SSRF
protection by automatically following redirects without re-validating the target
URL. Change the redirect option from 'follow' to 'manual' to prevent automatic
redirect following. After making this change, add logic to handle redirect
responses (HTTP 3xx status codes) by either rejecting them entirely if not
needed for your use case, or by validating each redirect target URL against your
SSRF protection rules before allowing the redirect to be followed.
🧹 Nitpick comments (1)
backend/src/schemas/monitors.schema.js (1)

8-13: 💤 Low value

Consider surfacing the specific rejection reason in the error message.

The current error message is generic. Zod 4's .refine() supports dynamic messages via function, which would help users understand why their URL was rejected (e.g., "Private IP" vs "Invalid hostname").

♻️ Optional improvement for dynamic error messages
   url: z.string().trim()
     .regex(urlPattern, 'Must be a valid HTTP or HTTPS URL with a domain')
-    .refine((val) => {
-      const result = validateUrlHostname(val);
-      return result.safe;
-    }, 'Private, reserved, or internal URLs are not allowed'),
+    .refine((val) => {
+      const result = validateUrlHostname(val);
+      return result.safe;
+    }, (val) => {
+      const result = validateUrlHostname(val);
+      return { message: result.reason || 'Private, reserved, or internal URLs are not allowed' };
+    }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/schemas/monitors.schema.js` around lines 8 - 13, The error
message in the refine method for the URL validation is static and does not
communicate the specific reason why a URL was rejected. Modify the second
argument of the refine call to use a function instead of a string literal that
takes the URL value as a parameter and returns a dynamic error message. Inside
this function, call validateUrlHostname to get the result object, then construct
and return a message that includes the specific rejection reason from the result
object rather than the generic 'Private, reserved, or internal URLs are not
allowed' message.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/utils/url-safety.js`:
- Around line 8-26: The isPrivateIPv4 function does not validate that each
parsed octet is within the valid IPv4 range of 0-255, allowing malformed inputs
like 192.168.1.256 or 10.abc.0.1 to potentially bypass security checks when they
produce NaN or out-of-range values. After splitting and mapping the IP string to
numbers, add an additional validation check to ensure each octet in the parts
array is a valid number and falls within the range 0-255. Add this validation
immediately after the length check to reject malformed inputs before processing
the private IP ranges.
- Around line 28-34: The isPrivateIPv6 function has incomplete IPv6 address
detection that creates an SSRF vulnerability by missing IPv4-mapped addresses
(like ::ffff:10.0.0.1), full-form representations with leading zeros, and
incomplete ULA range matching. Replace the manual string-based checks in
isPrivateIPv6 with a dedicated IPv6 parsing library such as ipaddr.js or
ip-address that properly handles RFC-compliant private address detection
including IPv4-mapped addresses, compressed and full-form representations, and
complete ULA range (fc00::/7) coverage.
- Around line 66-87: The resolveAndValidate function returns a resolved IP
address that is never used in the subsequent fetch request, creating a
time-of-check-time-of-use vulnerability where DNS can change between validation
and actual use. Modify the code to either pass the resolved IP from
resolveAndValidate to the fetch call in checks.service.js (by constructing the
URL with the IP address directly or using a custom DNS resolver) or implement IP
pinning at the socket level. Additionally, modify the lookup() call to use the {
all: true } option to validate all resolved addresses instead of only the first
one, ensuring that any hostname with multiple A records has all of them
validated for private IPs.

---

Outside diff comments:
In `@backend/src/services/checks.service.js`:
- Around line 22-26: The fetch request in the checks service uses redirect:
'follow' which bypasses SSRF protection by automatically following redirects
without re-validating the target URL. Change the redirect option from 'follow'
to 'manual' to prevent automatic redirect following. After making this change,
add logic to handle redirect responses (HTTP 3xx status codes) by either
rejecting them entirely if not needed for your use case, or by validating each
redirect target URL against your SSRF protection rules before allowing the
redirect to be followed.

---

Nitpick comments:
In `@backend/src/schemas/monitors.schema.js`:
- Around line 8-13: The error message in the refine method for the URL
validation is static and does not communicate the specific reason why a URL was
rejected. Modify the second argument of the refine call to use a function
instead of a string literal that takes the URL value as a parameter and returns
a dynamic error message. Inside this function, call validateUrlHostname to get
the result object, then construct and return a message that includes the
specific rejection reason from the result object rather than the generic
'Private, reserved, or internal URLs are not allowed' message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 54c904ab-941f-421d-9ee2-6a514ffef8d9

📥 Commits

Reviewing files that changed from the base of the PR and between 6b76216 and aaa6ceb.

📒 Files selected for processing (3)
  • backend/src/schemas/monitors.schema.js
  • backend/src/services/checks.service.js
  • backend/src/utils/url-safety.js

Comment thread backend/src/utils/url-safety.js Outdated
Comment thread backend/src/utils/url-safety.js Outdated
Comment thread backend/src/utils/url-safety.js
1. Concurrency limit: checks now run 50 at a time via p-limit
   instead of all-at-once, preventing socket/memory exhaustion
2. Retention job: daily cron at 3 AM deletes check_logs older
   than 30 days, preventing unbounded table growth
3. Fetch body drain: response.body.cancel() after reading status
   prevents TCP socket leaks under sustained check volume
4. IPv6 SSRF: replaced hand-rolled IPv6 checks with ipaddr.js
   which handles IPv4-mapped IPv6 (::ffff:169.254.169.254),
   full-form addresses, and all RFC edge cases

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/utils/url-safety.js (1)

71-76: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate all resolved IP addresses, not just the first.

lookup() without { all: true } returns only one address. A hostname with multiple A/AAAA records could have both public and private IPs, but only the first resolved address is validated. An attacker could configure DNS to return a public IP first (validated) followed by a private IP that might be used by the connection.

-    const { address } = await lookup(hostname);
-    if (isPrivateIP(address)) {
-      return { safe: false, reason: `Hostname resolves to a private IP (${address})` };
+    const addresses = await lookup(hostname, { all: true });
+    for (const { address } of addresses) {
+      if (isPrivateIP(address)) {
+        return { safe: false, reason: `Hostname resolves to a private IP (${address})` };
+      }
     }
-    return { safe: true, ip: address };
+    return { safe: true, ip: addresses[0].address };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/utils/url-safety.js` around lines 71 - 76, The lookup() call in
the hostname validation logic only returns and validates a single IP address.
Modify the lookup() call to include the { all: true } option so it returns all
resolved addresses for the hostname, then iterate through all returned addresses
to validate that none of them are private IPs using isPrivateIP() before
returning { safe: true }. If any address in the array is private, return the
unsafe result with the reason indicating which private IP was found.
🧹 Nitpick comments (2)
backend/src/utils/url-safety.js (1)

9-15: ⚡ Quick win

Consider adding carrierGradeNat to blocked ranges.

The ipaddr.js library also classifies 100.64.0.0/10 (RFC 6598 shared address space) as carrierGradeNat. In some cloud environments, this range could route to internal infrastructure. Consider adding it:

 const PRIVATE_RANGES = [
   'loopback',
   'private',
   'linkLocal',
   'uniqueLocal',
   'unspecified',
+  'carrierGradeNat',
 ];
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/utils/url-safety.js` around lines 9 - 15, The PRIVATE_RANGES
array in url-safety.js is missing the 'carrierGradeNat' classification which
represents RFC 6598 shared address space (100.64.0.0/10). Add 'carrierGradeNat'
as a string element to the PRIVATE_RANGES array definition alongside the
existing entries like 'loopback', 'private', 'linkLocal', 'uniqueLocal', and
'unspecified' to ensure this address range is properly blocked in cloud
environments where it could route to internal infrastructure.
backend/src/db/retention.queries.js (1)

6-10: Consider batched deletion for high-volume deployments.

With frequent monitor checks, this query could delete a large number of rows in a single transaction, potentially causing lock contention. For production deployments with high check volume:

  1. Ensure an index exists on check_logs.checked_at
  2. Consider batching deletes in chunks (e.g., DELETE ... LIMIT 10000 in a loop) to reduce lock duration

This is acceptable for initial deployment but may need revisiting at scale.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/db/retention.queries.js` around lines 6 - 10, The DELETE query
against check_logs that removes rows based on checked_at age performs a single
large deletion that can cause lock contention in high-volume deployments. To fix
this, ensure an index exists on the check_logs.checked_at column to optimize the
deletion query performance, then refactor the query execution to batch the
deletes in chunks (such as 10000 rows per iteration in a loop) rather than
deleting all matching rows in a single transaction. This approach reduces lock
duration and improves database stability at scale while maintaining the same
retention cleanup functionality.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@backend/src/scheduler/index.js`:
- Around line 22-24: The retention cleanup success message in the scheduler is
incorrectly using console.error instead of console.log, which pollutes error
streams and triggers unnecessary monitoring alerts. Change the console.error
call in the block that logs the deletion count to console.log since successful
deletion of old check logs is an informational message, not an error condition.

---

Outside diff comments:
In `@backend/src/utils/url-safety.js`:
- Around line 71-76: The lookup() call in the hostname validation logic only
returns and validates a single IP address. Modify the lookup() call to include
the { all: true } option so it returns all resolved addresses for the hostname,
then iterate through all returned addresses to validate that none of them are
private IPs using isPrivateIP() before returning { safe: true }. If any address
in the array is private, return the unsafe result with the reason indicating
which private IP was found.

---

Nitpick comments:
In `@backend/src/db/retention.queries.js`:
- Around line 6-10: The DELETE query against check_logs that removes rows based
on checked_at age performs a single large deletion that can cause lock
contention in high-volume deployments. To fix this, ensure an index exists on
the check_logs.checked_at column to optimize the deletion query performance,
then refactor the query execution to batch the deletes in chunks (such as 10000
rows per iteration in a loop) rather than deleting all matching rows in a single
transaction. This approach reduces lock duration and improves database stability
at scale while maintaining the same retention cleanup functionality.

In `@backend/src/utils/url-safety.js`:
- Around line 9-15: The PRIVATE_RANGES array in url-safety.js is missing the
'carrierGradeNat' classification which represents RFC 6598 shared address space
(100.64.0.0/10). Add 'carrierGradeNat' as a string element to the PRIVATE_RANGES
array definition alongside the existing entries like 'loopback', 'private',
'linkLocal', 'uniqueLocal', and 'unspecified' to ensure this address range is
properly blocked in cloud environments where it could route to internal
infrastructure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ecf60e6d-dbe9-45cc-8e24-00cf26898fdf

📥 Commits

Reviewing files that changed from the base of the PR and between aaa6ceb and 8437eff.

⛔ Files ignored due to path filters (1)
  • backend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (6)
  • backend/package.json
  • backend/src/db/retention.queries.js
  • backend/src/scheduler/index.js
  • backend/src/services/checks.service.js
  • backend/src/utils/url-safety.js
  • context1/learning.md
✅ Files skipped from review due to trivial changes (1)
  • context1/learning.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/package.json
  • backend/src/services/checks.service.js

Comment on lines +22 to +24
if (deleted > 0) {
console.error(`Retention: deleted ${deleted} check logs older than 30 days`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use console.log for informational messages.

Successful retention cleanup is not an error. Using console.error for this message will pollute error streams and monitoring alerts.

     if (deleted > 0) {
-      console.error(`Retention: deleted ${deleted} check logs older than 30 days`);
+      console.log(`Retention: deleted ${deleted} check logs older than 30 days`);
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (deleted > 0) {
console.error(`Retention: deleted ${deleted} check logs older than 30 days`);
}
if (deleted > 0) {
console.log(`Retention: deleted ${deleted} check logs older than 30 days`);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/scheduler/index.js` around lines 22 - 24, The retention cleanup
success message in the scheduler is incorrectly using console.error instead of
console.log, which pollutes error streams and triggers unnecessary monitoring
alerts. Change the console.error call in the block that logs the deletion count
to console.log since successful deletion of old check logs is an informational
message, not an error condition.

omarmenew24-collab and others added 2 commits June 21, 2026 12:02
- dns.lookup() now uses { all: true } to resolve every A/AAAA record.
  If ANY resolved IP is private, the check is blocked. Closes the
  multi-record bypass where one public + one private IP passes validation.
- Reverted IP pinning approach (broke HTTPS due to TLS cert/SNI mismatch).
- The narrow TOCTOU window between our DNS check and fetch's internal
  resolution is documented as an accepted limitation — true socket-level
  IP pinning requires custom undici internals, not justified for MVP.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Native fetch is available from Node 22+. Without this constraint,
a developer on Node 18/20 gets a cryptic "fetch is not defined" at
runtime. The engines field makes npm warn on install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@omarmenew24-collab
omarmenew24-collab merged commit e737b30 into main Jun 21, 2026
1 check passed
@omarmenew24-collab

Copy link
Copy Markdown
Owner Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

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.

1 participant