refactor: replace axios with native fetch#397
Conversation
WalkthroughSwaps axios for the global fetch across docs, types, runtime, and tests; introduces a FetchOptions type and Node.js >=18 engine requirement; removes axios dependency; adds a fromURL-focused test suite and tsconfig; minor whitespace fix in ical.js. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Caller
participant NodeICal as node-ical.async.fromURL
participant Fetch as global fetch
participant Parser as parseICS
Caller->>NodeICal: fromURL(url, [options], [callback])
Note over NodeICal: Normalize args → build RequestInit (from FetchOptions)
NodeICal->>Fetch: fetch(url, RequestInit)
Fetch-->>NodeICal: Response
alt response.ok
NodeICal->>Fetch: response.text()
Fetch-->>NodeICal: icsText
NodeICal->>Parser: parseICS(icsText)
Parser-->>NodeICal: CalendarResponse
NodeICal-->>Caller: resolve/emit CalendarResponse
else non-ok
NodeICal-->>Caller: throw/reject Error(status, statusText)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
🚧 Files skipped from review as they are similar to previous changes (2)
🔇 Additional comments (4)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (5)
README.md (2)
87-91: Add a timeout example for fetch options.Fetch has no default timeout; show an AbortController pattern to prevent hangs when endpoints stall.
- // you can pass standard fetch() options (e.g. headers) + // you can pass standard fetch() options (e.g. headers, signal for timeout) + // Example: 5s timeout + const ac = new AbortController(); + setTimeout(() => ac.abort(), 5000); const headerWebEvents = await ical.async.fromURL( 'http://lanyrd.com/topics/nodejs/nodejs.ics', - { headers: { 'User-Agent': 'API-Example / 1.0' } } + { headers: { 'User-Agent': 'API-Example/1.0' }, signal: ac.signal } );
196-196: Clarify Node 16 fallback with a safe CJS-friendly polyfill snippet.Current note is great; add a one-liner that works from CommonJS without pinning node-fetch v2.
-Fetch the specified URL using the native fetch API (```options``` are passed to the underlying `fetch()` call) and call the function with the result (either an error or the data). Requires Node.js 18+ (or any environment that provides a global `fetch`). +Fetch the specified URL using the native fetch API (```options``` are passed to the underlying `fetch()` call) and call the function with the result (either an error or the data). Requires Node.js 18+ (or any environment that provides a global `fetch`). + +For older Node 16 setups, you can polyfill: +```js +// At app startup +global.fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); +```node-ical.js (1)
102-110: Timeouts and clearer missing‑fetch error.
- Add optional timeout via AbortController for reliability.
- Prefer explicit
globalThis.fetchand a helpful error iffetchis unavailable.- fetch(url, fetchOptions) + if (typeof globalThis.fetch !== 'function') { + reject(new Error('Global fetch is not available. Requires Node.js >=18 or a fetch polyfill.')); + return; + } + + // Optional timeout (ms) if provided by caller + let timeoutId; + if (typeof fetchOptions.timeout === 'number') { + const ac = new AbortController(); + fetchOptions.signal = fetchOptions.signal ?? ac.signal; + timeoutId = setTimeout(() => ac.abort(new Error('fetch timeout')), fetchOptions.timeout); + delete fetchOptions.timeout; // not a standard fetch option + } + + globalThis.fetch(url, fetchOptions) .then(response => { if (!response.ok) { // Mimic previous error style throw new Error(`${response.status} ${response.statusText}`); } - return response.text(); + return response.text(); }) .then(data => { + if (timeoutId) clearTimeout(timeoutId); ical.parseICS(data, (error, ics) => {node-ical.d.ts (1)
4-14: Broaden FetchOptions.headers to avoid TS friction without DOM libs.Current union rejects
Headersinstances in TS projects. Widening keeps strong typing while avoiding DOM dependency.export interface FetchOptions { method?: string; - headers?: Record<string, string> | Array<[string, string]>; - body?: any; - // Allow any additional fields (e.g. agent for node, redirect etc.) without forcing DOM lib. - [key: string]: any; + // Accept common shapes without importing DOM types + headers?: Record<string, string> | Array<[string, string]> | Iterable<[string, string]> | unknown; + body?: unknown; + // Allow Node-specific fields like `agent`, plus custom ones (e.g., `timeout`) + [key: string]: unknown; }package.json (1)
20-22: CI already runs Node ≥18; engines entry matches.github/workflows/nodejs.yml matrix includes node-version: [18.x, 20.x, 22.x, 24.x] and uses actions/setup-node@v4. Optional: add engine-strict=true to .npmrc to fail installs on older Node.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (4)
README.md(2 hunks)node-ical.d.ts(3 hunks)node-ical.js(3 hunks)package.json(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
node-ical.js (1)
cvt-to-json.js (1)
fs(3-3)
🔇 Additional comments (3)
node-ical.js (1)
1-1: LGTM: use ofnode:fsimport path.Modern, explicit Node core import.
node-ical.d.ts (2)
30-31: Async overloads look correct and match JS behavior.Covers callback, (url, options, cb), and Promise forms.
44-46: Top-level fromURL overloads align with async variant.Consistent surface area; no breaking type changes for existing callers.
|
I will check the comments from Code Rabbit soon 🙂 |
- update `xo` (the old version depended on a deprecated node function) - disabling all complaining rules to not to overload the PR with thousands of changes - fixed two simple linter issues that couldn't be easily resolved by disabling rules
|
@KristjanESPERANTO Thanks for this great PR. I was always looking for replacing axios at some point here. Great that you walked on that task and has it almost finished. I just allowed the github workflows to run to verify your changes won't break things. So please review the checks. It seems your changes breaks the windows-based executions/tests. So please fix. In addition, please have a look for the comment from github-advanced-security on your added |
Mainly to make "github-advanced-security" check happy.
|
@jens-maus Thanks for the positive feedback 🙂 I think I've solved the problems. Please run the GitHub workflows again. |
|
Thanks for the quickly reply and thanks for your changes. Looks better now, but still two checks fail. Please see if you can fix them as well. |
To silence CodeQL dynamic lookup alert.
…4 libuv assertion
6a7f6fb to
9f8fa2e
Compare
…and maintainability
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
But it is still a "breaking "change because in fact we allowed to use "all kind of axios GET options" before and thats now "fetch options", or are they 100% compatible? So in my eyes that could still require a major version bump, because noone knows which "fancy options" the devs were using that used this lib. But other than that fully agreed. |
There was a problem hiding this comment.
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)
node-ical.d.ts (1)
65-65: Fix callback nullability:datacan benullon errorsRuntime behavior (and tests) pass
nullfordatawhenerroris set. The type must reflect this to prevent unsafe dereferences for TS consumers.- export type NodeIcalCallback = (error: any, data: CalendarResponse) => void; + export type NodeIcalCallback = (error: any, data: CalendarResponse | null) => void;
🧹 Nitpick comments (4)
test/test-fromURL.js (4)
97-99: Use strict assertions; avoid coercionPrefer
assert.strictEqualto prevent accidental type coercion in tests.- assert.equal(ev.summary, 'Fetch Test Event'); + assert.strictEqual(ev.summary, 'Fetch Test Event'); @@ - assert.equal(data, null); + assert.strictEqual(data, null); @@ - assert.equal(ev.summary, 'Secured Event'); + assert.strictEqual(ev.summary, 'Secured Event'); @@ - assert.equal(ev.summary, 'Callback No Options'); + assert.strictEqual(ev.summary, 'Callback No Options'); @@ - assert.equal(data, null); + assert.strictEqual(data, null); @@ - assert.equal(ev.summary, 'Promise Event'); + assert.strictEqual(ev.summary, 'Promise Event'); @@ - assert.strictEqual(ok, true); + assert.strictEqual(ok, true);Also applies to: 121-122, 151-156, 181-186, 204-208, 235-240, 261-264
105-109: Silence unused param warnings for consistencyA few route handlers still use an unused
requestparam; standardize on_requestlike elsewhere.- '/missing.ics'(request, res) { + '/missing.ics'(_request, res) { @@ - '/plain-missing.ics'(request, res) { + '/plain-missing.ics'(_request, res) { @@ - '/missing.ics'(request, res) { + '/missing.ics'(_request, res) {Also applies to: 167-170, 191-194
86-90: Call test callbacks before teardown to avoid edge‑case racesMinor ordering tweak: invoke
this.callback(...)first, thendone(); teardown may occasionally preempt callbacks on slow CI.Example (apply similarly to all topics):
- ical.fromURL(`${urlBase}/ok.ics`, {}, (error, data) => { - done(); - this.callback(error, data); - }); + ical.fromURL(`${urlBase}/ok.ics`, {}, (error, data) => { + this.callback(error, data); + done(); + });Also applies to: 111-115, 143-147, 174-177, 197-201, 224-231
27-45: Harden server teardown to reduce Windows CI flakinessDisable keep‑alive and tighten header timeout to avoid lingering sockets; give the close a slightly longer grace period.
Apply:
const server = http.createServer((req, res) => { @@ }); + // Reduce flakiness on Windows/Node by avoiding keep-alive lingering + server.keepAliveTimeout = 0; + server.headersTimeout = 1000; + @@ - setTimeout(() => server.close(), 10); + setTimeout(() => server.close(), 25);Then re-run the Windows workflow to confirm stability.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
README.md(2 hunks)ical.js(1 hunks)node-ical.d.ts(3 hunks)node-ical.js(4 hunks)package.json(2 hunks)test/test-fromURL.js(1 hunks)tsconfig.json(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- ical.js
🚧 Files skipped from review as they are similar to previous changes (3)
- README.md
- node-ical.js
- package.json
🔇 Additional comments (1)
node-ical.d.ts (1)
4-19: Optional: tighten FetchOptions via undici's RequestInit — verification inconclusiveProvided script output was inconclusive; confirm 'undici' (and its types) is present in CI (check package.json/lockfile or run: node -e 'console.log(require.resolve("undici"))') — if absent, keep the current minimal interface.
- export interface FetchOptions { + // import type only; no runtime dependency + // import type { RequestInit } from 'undici'; + export interface FetchOptions /* extends Partial<RequestInit> */ { method?: string; @@ [key: string]: unknown; }Also applies to: 36-37, 50-52
|
I could have tested the workflows in my fork beforehand, sorry. I've done that now and the tests should work. But next, I'll take a look at the new CodeRabit comments.
I doubt that. And I also would recommend a major version bump. |
Only the CodeQL run is still producing an error. Please review.
Definitly a good idea, yes.
I also agree and have planned to do a major bump. @Apollon77 Do you know if we are using custom axios option sin ioBroker.ical which is using node-ical? |
… assertions for consistency
fromurl() is not used because ioBroker has own logic to fetch the file including caching and such, so issue here, but maybe for others |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
|
LGTM now! Perfect. So all we have to do is get the other things done, bump the version to 0.21.0. Thus, the packages that use node-ical most are ioBroker.ical and MagicMirror. If both are fine with these changes, I think we shouldn't have any worries to consider... |
|
I ran the MagicMirror e2e tests with my fork without an issue. So that side seems fine 🙂 |
Since receiving a security warning about `axios` in `node-ical` (https://github.com/MagicMirrorOrg/MagicMirror/security/dependabot/70), I've created [a pull request to remove `axios` from `node-ical`](jens-maus/node-ical#397) — it was accepted and we now have a new version 🙂
Why
axiosdependency has had repeated security advisories and added supply‑chain surface. Current example: https://vulert.com/vuln-db/CVE-2025-58754.fetch.axiosinnode-icalis minimal (simple GET + body text) and doesn't rely on advanced features (interceptors, automatic JSON, cancel tokens, etc.).axios:Impact on users
With this PR the exported API, function signatures, and callback/promise behavior are unchanged. For consumers already on Node.js 18 or newer, no application code modifications are required.
For older Node 16 setups, you can polyfill:
Summary by CodeRabbit
Refactor
Types
Documentation
Tests
Chores