Skip to content

fix: surface network cause in cron fetch failures - #16

Merged
Bonobo791 merged 1 commit into
mainfrom
fix-cron-fetch-logging
Jul 31, 2026
Merged

fix: surface network cause in cron fetch failures#16
Bonobo791 merged 1 commit into
mainfrom
fix-cron-fetch-logging

Conversation

@Bonobo791

Copy link
Copy Markdown
Owner

Why

The 00:15 UTC scheduled run failed 3× (schedule + retries) with only TypeError: fetch failed in the logs — undici hides the actual reason (DNS, TLS, connection refused, timeout) in error.cause, which Netlify's log line doesn't print.

What

netlify/functions/cron.mjs: the fetch is wrapped so a network-level rejection is rethrown as cron endpoint unreachable: fetch failed (<code>: <message>). After merge, the next failing invocation will name the real cause instead of the generic wrapper.

Test

New case in netlify/cron.test.mjs: fetch rejects with TypeError('fetch failed', { cause }) → thrown message contains both 'fetch failed' and the cause code. Watched fail before the fix, pass after.

Verification

  • npm run test: 91/91 (16 files)
  • npm run check: 0 errors
  • npm run build: green

…able

Production failed invocations logged only 'TypeError: fetch failed';
undici wraps the real reason (DNS, TLS, refused, timeout) in error.cause.
Rethrow with the cause code and message so failed scheduled runs are
diagnosable from Netlify logs alone. Reproducing test committed in the
same change (watched fail, then pass).
@codeant-ai

codeant-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown

🤖 CodeAnt AI — Review Status

Status Commit Started (UTC) Finished (UTC)
✅ Reviewed your PR 81afb94 Jul 31, 2026 · 00:22 00:24

@cla-bot cla-bot Bot added the cla-signed label Jul 31, 2026
@netlify

netlify Bot commented Jul 31, 2026

Copy link
Copy Markdown

Deploy Preview for moderaty ready!

Name Link
🔨 Latest commit 81afb94
🔍 Latest deploy log https://app.netlify.com/projects/moderaty/deploys/6a6bead2159e3e00083fe9ef
😎 Deploy Preview https://deploy-preview-16--moderaty.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@Bonobo791, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 4 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c9610d36-49bc-403b-a23f-02fc210ed920

📥 Commits

Reviewing files that changed from the base of the PR and between b536f4f and 81afb94.

📒 Files selected for processing (2)
  • netlify/cron.test.mjs
  • netlify/functions/cron.mjs

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

@codeant-ai codeant-ai Bot added the size:S This PR changes 10-29 lines, ignoring generated files label Jul 31, 2026
@codeant-ai

codeant-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown

User description

Why

The 00:15 UTC scheduled run failed 3× (schedule + retries) with only TypeError: fetch failed in the logs — undici hides the actual reason (DNS, TLS, connection refused, timeout) in error.cause, which Netlify's log line doesn't print.

What

netlify/functions/cron.mjs: the fetch is wrapped so a network-level rejection is rethrown as cron endpoint unreachable: fetch failed (<code>: <message>). After merge, the next failing invocation will name the real cause instead of the generic wrapper.

Test

New case in netlify/cron.test.mjs: fetch rejects with TypeError('fetch failed', { cause }) → thrown message contains both 'fetch failed' and the cause code. Watched fail before the fix, pass after.

Verification

  • npm run test: 91/91 (16 files)
  • npm run check: 0 errors
  • npm run build: green

CodeAnt-AI Description

Surface the underlying cause when scheduled cron requests cannot reach the endpoint

What Changed

  • Failed cron fetches now include the underlying network reason, such as DNS failure, TLS error, connection refusal, or timeout, in the error message
  • Added coverage confirming that network cause codes appear alongside the original fetch failure

Impact

✅ Clearer scheduled-run failure logs
✅ Faster diagnosis of unreachable cron endpoints

💡 Usage Guide

Checking Your Pull Request

Every time you make a pull request, our system automatically looks through it. We check for security issues, mistakes in how you're setting up your infrastructure, and common code problems. We do this to make sure your changes are solid and won't cause any trouble later.

Talking to CodeAnt AI

Got a question or need a hand with something in your pull request? You can easily get in touch with CodeAnt AI right here. Just type the following in a comment on your pull request, and replace "Your question here" with whatever you want to ask:

@codeant-ai ask: Your question here

This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code.

Example

@codeant-ai ask: Can you suggest a safer alternative to storing this secret?

Preserve Org Learnings with CodeAnt

You can record team preferences so CodeAnt AI applies them in future reviews. Reply directly to the specific CodeAnt AI suggestion (in the same thread) and replace "Your feedback here" with your input:

@codeant-ai: Your feedback here

This helps CodeAnt AI learn and adapt to your team's coding style and standards.

Example

@codeant-ai: Do not flag unused imports.

Retrigger review

Ask CodeAnt AI to review the PR again, by typing:

@codeant-ai: review

Check Your Repository Health

To analyze the health of your code repository, visit our dashboard at https://app.codeant.ai. This tool helps you identify potential issues and areas for improvement in your codebase, ensuring your repository maintains high standards of code health.

@sonarqubecloud

Copy link
Copy Markdown

@amazon-q-developer amazon-q-developer 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.

This is a clean fix for improving observability of network failures in the cron function. The implementation correctly extracts and surfaces the underlying cause from fetch errors, includes proper defensive checks for undefined values, and adds comprehensive test coverage.

The code is ready to merge - all error handling paths are properly covered, and the test case accurately simulates the undici error structure.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

@codeant-ai

codeant-ai Bot commented Jul 31, 2026

Copy link
Copy Markdown

🏁 CodeAnt Quality Gate Results

Commit: 81afb948
Scan Time: 2026-07-31 00:23:57 UTC

✅ Overall Status: PASSED

Quality Gate Details

Quality Gate Status Details
Secrets ✅ PASSED 0 secrets found
Duplicate Code ✅ PASSED 0.0% duplicated
SAST ✅ PASSED No security issues
Bugs ✅ PASSED Rating S: No bugs
IAC ✅ PASSED No IAC issues
Antipatterns ✅ PASSED No antipatterns

View Full Results

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Surface undici network cause in cron fetch failures

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Rethrow cron fetch failures with the underlying undici error.cause details.
• Make Netlify logs actionable by including network error code/message.
• Add a regression test for TypeError('fetch failed', { cause }) formatting.
Diagram

graph TD
  A["Netlify Scheduler"] --> B["netlify/functions/cron.mjs"] --> C["fetch(...)"] --> D["Cron endpoint"]
  C --> E["Wrap error w/ cause"] --> F["Netlify logs"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Log original error and cause (no rethrow formatting)
  • ➕ Preserves original stack and error type
  • ➕ Less string formatting logic
  • ➖ Netlify log aggregation may still omit nested cause details
  • ➖ Requires consistent logging patterns and may still be hard to search/alert on
2. Use `new Error(msg, { cause })` (keep structured cause)
  • ➕ Keeps cause machine-readable for higher-level handlers
  • ➕ Avoids manual string concatenation for detail propagation
  • ➖ Netlify may still print only the top-level message, losing the benefit
  • ➖ Runtime support/behavior for cause printing can vary across environments
3. Introduce a dedicated `CronEndpointUnreachableError` class
  • ➕ Clearer semantics; can be matched/handled separately
  • ➕ Encapsulates formatting + metadata cleanly
  • ➖ More code surface area than needed for a logging-focused fix
  • ➖ Still likely needs message formatting for Netlify visibility

Recommendation: Keep the current approach: embedding the cause code/message into the thrown error string is the most reliable way to make Netlify logs actionable when error.cause is otherwise hidden. Consider additionally setting { cause } on the new error in the future if structured handling becomes useful, but the current message-based solution best matches the stated goal (diagnosable logs).

Files changed (2) +20 / -0

Bug fix (1) +8 / -0
cron.mjsRethrow fetch failures with undici cause details in message +8/-0

Rethrow fetch failures with undici cause details in message

• Wraps the cron endpoint 'fetch' in a try/catch and rethrows a new error that includes the original error message plus 'error.cause' code/name and message when available. This makes scheduled-run failures diagnosable from Netlify logs without needing nested error printing.

netlify/functions/cron.mjs

Tests (1) +12 / -0
cron.test.mjsAdd regression test for surfacing fetch network cause +12/-0

Add regression test for surfacing fetch network cause

• Adds a test that stubs 'fetch' to throw 'TypeError('fetch failed', { cause })' and asserts the handler's error message includes both the generic failure and the cause code (e.g., 'ENOTFOUND'). This ensures DNS/TLS/refused/timeout details remain visible after wrapping.

netlify/cron.test.mjs

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 6 complexity · 0 duplication

Metric Results
Complexity 6 (≤ 100 complexity)
Duplication 0 (≤ 1 duplication)

View in Codacy

AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

@Bonobo791
Bonobo791 merged commit eb11dc3 into main Jul 31, 2026
15 of 16 checks passed
@Bonobo791
Bonobo791 deleted the fix-cron-fetch-logging branch July 31, 2026 00:24

@codacy-production codacy-production 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.

Pull Request Overview

The implementation successfully surfaces underlying network causes (like DNS or TLS errors) by extracting the cause property from fetch errors. This improves observability for cron failures.

However, a logic issue exists where the 25-second timeout is cleared before the response body is fully read. This could lead to function hangs if the network connection stalls during the data transfer. Additionally, while the primary success path is tested, edge cases for errors without a cause or non-Error rejections are not covered.

Codacy analysis indicates the PR is up to standards with no new static analysis issues.

About this PR

  • The current test suite covers the primary use case (DNS failures/TypeError with cause), but lacks coverage for scenarios where fetch might reject with a standard Error (missing a cause property) or a non-Error object. Ensuring these are handled gracefully will prevent the surfacing logic from causing secondary crashes.

Test suggestions

  • Fetch rejects with a TypeError containing an Error object as the cause (e.g., DNS failure)
  • Fetch rejects with an Error that does not contain a cause property
  • Fetch rejects with a non-Error object
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Fetch rejects with an Error that does not contain a cause property
2. Fetch rejects with a non-Error object

TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback

headers: { authorization: `Bearer ${secret}` },
signal: controller.signal
});
} catch (error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 MEDIUM RISK

Try running the following prompt in your coding agent: Move the res.text() call inside the try block and ensure clearTimeout(timer) is only called after the body has been fully read. This ensures the 25s timeout covers the entire request lifecycle and that streaming failures also benefit from detailed error surfacing.

const cause = error instanceof Error ? error.cause : undefined;
const detail = cause instanceof Error ? `${cause.code ?? cause.name}: ${cause.message}` : 'no cause';
throw new Error(
`cron endpoint unreachable: ${error instanceof Error ? error.message : String(error)} (${detail})`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ LOW RISK

Suggestion: Include the error type in the message by using String(error) instead of error.message to provide better context in the logs.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Context used
✅ Compliance rules (platform): 51 rules

Grey Divider


Remediation recommended

1. Wrapped error drops cause 🐞 Bug ◔ Observability
Description
In cron(), the new catch always throws a fresh Error without attaching the caught exception as
its cause, so the original error’s type/stack/properties can’t be inspected via standard error
chaining. This reduces debugging/classification fidelity (only the constructed message remains).
Code

netlify/functions/cron.mjs[R42-49]

+	} catch (error) {
+		// undici hides the real network reason (DNS, TLS, refused) in `cause`;
+		// surface it so failed invocations are diagnosable from the logs alone.
+		const cause = error instanceof Error ? error.cause : undefined;
+		const detail = cause instanceof Error ? `${cause.code ?? cause.name}: ${cause.message}` : 'no cause';
+		throw new Error(
+			`cron endpoint unreachable: ${error instanceof Error ? error.message : String(error)} (${detail})`
+		);
Relevance

●● Moderate

No close precedent on Error.cause chaining; change is small but may be viewed as unnecessary for
Netlify logs.

PR-#13

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The PR-added catch rethrows a newly constructed Error and does not use { cause: error }, so
the original thrown value is not preserved as an error chain.

netlify/functions/cron.mjs[37-52]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`cron()` wraps `fetch()` failures in a new `Error(...)` but does not set the caught exception as the new error’s `cause`, losing the original error chain (type/stack/metadata).

### Issue Context
Node supports error chaining via `new Error(message, { cause })`. Preserving the original error object keeps richer diagnostics while still allowing you to customize the message for Netlify logs.

### Fix Focus Areas
- netlify/functions/cron.mjs[42-49]

### Suggested fix
- Build the message exactly as you do now, but throw with `cause`:
 - If `error` is an `Error`, use `new Error(message, { cause: error })`.
 - If `error` is not an `Error`, wrap it first (`new Error(String(error))`) and use that as the cause.
- Keep your `detail` string for readability, but preserve the original exception chain via `cause`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Unbounded cause detail 🐞 Bug ◔ Observability
Description
The new wrapper message embeds cause.message verbatim, so an unusually large nested-cause message
can create very large thrown messages/log lines. This is inconsistent with the same file’s explicit
truncation of cron response bodies to keep logs bounded.
Code

netlify/functions/cron.mjs[R45-49]

+		const cause = error instanceof Error ? error.cause : undefined;
+		const detail = cause instanceof Error ? `${cause.code ?? cause.name}: ${cause.message}` : 'no cause';
+		throw new Error(
+			`cron endpoint unreachable: ${error instanceof Error ? error.message : String(error)} (${detail})`
+		);
Relevance

●●● Strong

Team recently accepted bounding/truncating cron error/log output to avoid noisy/leaky logs.

PR-#13

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new code interpolates cause.message directly into the thrown string, while later code
explicitly truncates response bodies to 500 characters to bound logs.

netlify/functions/cron.mjs[42-49]
netlify/functions/cron.mjs[53-56]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The wrapper error message includes `cause.message` (and `error.message`) with no length bound, which can create oversized log/error lines.

### Issue Context
This file already truncates the response body before logging/throwing, indicating a desire to keep Netlify logs bounded.

### Fix Focus Areas
- netlify/functions/cron.mjs[45-49]

### Suggested fix
- Truncate `error.message` and `cause.message` (e.g., to 200–500 chars) before interpolating into the thrown message.
- Optionally reuse a small helper like `const clip = (s, n) => String(s).slice(0, n)` to keep the logic consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +42 to +49
} catch (error) {
// undici hides the real network reason (DNS, TLS, refused) in `cause`;
// surface it so failed invocations are diagnosable from the logs alone.
const cause = error instanceof Error ? error.cause : undefined;
const detail = cause instanceof Error ? `${cause.code ?? cause.name}: ${cause.message}` : 'no cause';
throw new Error(
`cron endpoint unreachable: ${error instanceof Error ? error.message : String(error)} (${detail})`
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Wrapped error drops cause 🐞 Bug ◔ Observability

In cron(), the new catch always throws a fresh Error without attaching the caught exception as
its cause, so the original error’s type/stack/properties can’t be inspected via standard error
chaining. This reduces debugging/classification fidelity (only the constructed message remains).
Agent Prompt
### Issue description
`cron()` wraps `fetch()` failures in a new `Error(...)` but does not set the caught exception as the new error’s `cause`, losing the original error chain (type/stack/metadata).

### Issue Context
Node supports error chaining via `new Error(message, { cause })`. Preserving the original error object keeps richer diagnostics while still allowing you to customize the message for Netlify logs.

### Fix Focus Areas
- netlify/functions/cron.mjs[42-49]

### Suggested fix
- Build the message exactly as you do now, but throw with `cause`:
  - If `error` is an `Error`, use `new Error(message, { cause: error })`.
  - If `error` is not an `Error`, wrap it first (`new Error(String(error))`) and use that as the cause.
- Keep your `detail` string for readability, but preserve the original exception chain via `cause`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +45 to +49
const cause = error instanceof Error ? error.cause : undefined;
const detail = cause instanceof Error ? `${cause.code ?? cause.name}: ${cause.message}` : 'no cause';
throw new Error(
`cron endpoint unreachable: ${error instanceof Error ? error.message : String(error)} (${detail})`
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

2. Unbounded cause detail 🐞 Bug ◔ Observability

The new wrapper message embeds cause.message verbatim, so an unusually large nested-cause message
can create very large thrown messages/log lines. This is inconsistent with the same file’s explicit
truncation of cron response bodies to keep logs bounded.
Agent Prompt
### Issue description
The wrapper error message includes `cause.message` (and `error.message`) with no length bound, which can create oversized log/error lines.

### Issue Context
This file already truncates the response body before logging/throwing, indicating a desire to keep Netlify logs bounded.

### Fix Focus Areas
- netlify/functions/cron.mjs[45-49]

### Suggested fix
- Truncate `error.message` and `cause.message` (e.g., to 200–500 chars) before interpolating into the thrown message.
- Optionally reuse a small helper like `const clip = (s, n) => String(s).slice(0, n)` to keep the logic consistent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cla-signed size:S This PR changes 10-29 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant