fix: surface network cause in cron fetch failures - #16
Conversation
…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 — Review Status
|
✅ Deploy Preview for moderaty ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Warning Review limit reached
Next review available in: 4 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Comment |
User descriptionWhyThe 00:15 UTC scheduled run failed 3× (schedule + retries) with only What
TestNew case in Verification
CodeAnt-AI DescriptionSurface the underlying cause when scheduled cron requests cannot reach the endpoint What Changed
Impact
💡 Usage GuideChecking Your Pull RequestEvery 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 AIGot 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: This lets you have a chat with CodeAnt AI about your pull request, making it easier to understand and improve your code. ExamplePreserve Org Learnings with CodeAntYou 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: This helps CodeAnt AI learn and adapt to your team's coding style and standards. ExampleRetrigger reviewAsk CodeAnt AI to review the PR again, by typing: Check Your Repository HealthTo 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. |
|
There was a problem hiding this comment.
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 Quality Gate ResultsCommit: ✅ Overall Status: PASSEDQuality Gate Details
|
PR Summary by QodoSurface undici network cause in cron fetch failures
AI Description
Diagram
High-Level Assessment
Files changed (2)
|
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | ✅ 6 (≤ 100 complexity) |
| Duplication | ✅ 0 (≤ 1 duplication) |
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
🟡 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})` |
There was a problem hiding this comment.
⚪ 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.
Code Review by Qodo
Context used✅ Compliance rules (platform):
51 rules 1. Wrapped error drops cause
|
| } 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})` | ||
| ); |
There was a problem hiding this comment.
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
| 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})` | ||
| ); |
There was a problem hiding this comment.
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



Why
The 00:15 UTC scheduled run failed 3× (schedule + retries) with only
TypeError: fetch failedin the logs — undici hides the actual reason (DNS, TLS, connection refused, timeout) inerror.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 ascron 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 withTypeError('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 errorsnpm run build: green