Skip to content

refactor: replace axios with native fetch#397

Merged
jens-maus merged 15 commits intojens-maus:masterfrom
KristjanESPERANTO:fetch
Sep 14, 2025
Merged

refactor: replace axios with native fetch#397
jens-maus merged 15 commits intojens-maus:masterfrom
KristjanESPERANTO:fetch

Conversation

@KristjanESPERANTO
Copy link
Contributor

@KristjanESPERANTO KristjanESPERANTO commented Sep 13, 2025

Why

  • The axios dependency has had repeated security advisories and added supply‑chain surface. Current example: https://vulert.com/vuln-db/CVE-2025-58754.
  • All actively supported Node.js versions (≥ 18) now include a stable, standards‑compliant global fetch.
  • The usage of axios in node-ical is minimal (simple GET + body text) and doesn't rely on advanced features (interceptors, automatic JSON, cancel tokens, etc.).
  • Removing axios:
    • Eliminates an external dependency
    • Reduces install size / cold start time
    • Lowers maintenance and audit noise

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:

// At app startup
global.fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));

Summary by CodeRabbit

  • Refactor

    • Switched URL fetching to native fetch; options now follow RequestInit semantics and require Node.js >=18.
  • Types

    • Introduced a minimal FetchOptions type for public signatures and adjusted callback typing to allow undefined data.
  • Documentation

    • README updated with fetch usage, AbortController timeout example, header example, and environment note.
  • Tests

    • Added tests covering fetch success, 404/error handling, header passthrough, callback and promise modes.
  • Chores

    • Removed axios dependency; updated linter tooling/config, package engines, and test script.

@coderabbitai
Copy link

coderabbitai bot commented Sep 13, 2025

Walkthrough

Swaps 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

Cohort / File(s) Summary
Documentation
README.md
Replace axios guidance with fetch usage notes, add AbortController timeout example, update header example and Node.js 18+ requirement.
Type Declarations
node-ical.d.ts
Add FetchOptions interface; replace AxiosRequestConfig usages with FetchOptions in fromURL overloads; allow callback data to be `CalendarResponse
Runtime HTTP Fetch
node-ical.js
Replace axios.get with fetch(url, fetchOptions); build RequestInit from provided options; check response.ok, use response.text(); normalize (url, cb) and (url, options, cb) calling patterns; update JSDoc to describe fetch options.
Tests
test/test-fromURL.js
New vows test suite using a native HTTP server to exercise fromURL: success, 404, header passthrough, callback vs Promise, and teardown/socket cleanup.
Package & Tooling
package.json, tsconfig.json
Add engines.node: ">=18"; remove axios dependency; bump xo devDependency and add rule overrides and pre-commit hook; update test script to include test/test-fromURL.js; add tsconfig.json with JS/TS settings.
Minor Fix
ical.js
Adjust indentation inside conditional that clears recurrenceid; no behavioral change.

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

I hopped from axios lanes to breezy fetch,
AbortController held the timeout stitch,
Node eighteen skies keep my burrow bright,
Types trimmed, tests spun, sockets sleeping tight. 🐇✨

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 "refactor: replace axios with native fetch" is a concise, single-sentence summary that accurately reflects the PR's primary change (removing axios in favor of the native fetch API) and aligns with the provided changeset and objectives, making it clear for a teammate scanning history.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 64e503f and c9ee261.

📒 Files selected for processing (3)
  • node-ical.d.ts (4 hunks)
  • test/test-fromURL.js (1 hunks)
  • tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tsconfig.json
  • test/test-fromURL.js
🔇 Additional comments (4)
node-ical.d.ts (4)

50-50: Top‑level overload mirrors async variant correctly.

Signatures align; no further action.


65-65: Prefer optional result and safer error typing: (err: unknown, data?: CalendarResponse)

More idiomatic Node callback shape and nudges consumers to check for undefined.

-export type NodeIcalCallback = (error: any, data: CalendarResponse | undefined) => void;
+export type NodeIcalCallback = (error: unknown, data?: CalendarResponse) => void;

Confirm tests cover callback paths with both error and success to validate the typing change.


4-19: Broaden headers typing; tighten FetchOptions comment.

Accept tuple arrays and readonly tuples for headers; prefer standard RequestInit and Node fetch extensions (e.g., dispatcher, compress) and avoid implying non‑standard numeric options (follow/agent) unless bridged at runtime. Verify node-ical.d.ts (lines 4–19) and apply the diff below.

   export interface FetchOptions {
     method?: string;
     /**
      * Accept common header container shapes without depending on DOM lib types.
      * - Plain object map
-     * - Any iterable of [key,value] tuples (covers Arrays and WHATWG Headers at runtime)
+     * - Tuple arrays or any iterable of [key,value] tuples (covers Arrays and WHATWG Headers at runtime)
      */
-    headers?: Record<string, string> | Iterable<[string, string]>;
+    headers?:
+      | Record<string, string>
+      | Array<[string, string]>
+      | ReadonlyArray<readonly [string, string]>
+      | string[][]
+      | Iterable<readonly [string, string]>;
     /** Request body (caller supplied) */
     body?: unknown;
-    /** Additional fetch options (e.g. agent, redirect, follow, timeout, signal, etc.) */
+    /** Additional options are forwarded as‑is to the underlying fetch. Prefer standard RequestInit (e.g. redirect, signal) and Node extensions (e.g. dispatcher, compress). */
     [key: string]: unknown;
   }

36-36: Overload swap to FetchOptions looks good; confirm consumer impact.

  • node-ical.d.ts now exposes a permissive FetchOptions (includes [key: string]: unknown and documents agent/timeout/follow/etc.) and the fromURL runtime overloads are unchanged.
  • No in-repo callers of fromURL were found, but ripgrep returned "No files were searched" for some queries, so caller search is inconclusive—confirm there are no internal or external consumers passing an AxiosRequestConfig (this remains a type-level breaking change; fine for a major bump).
  • Optional: add the shallow compatibility alias from the original comment to soften the transition if you want to accept broader Axios shapes.

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.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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.fetch and a helpful error if fetch is 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 Headers instances 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

📥 Commits

Reviewing files that changed from the base of the PR and between 6c989aa and ebc180c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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 of node:fs import 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.

@KristjanESPERANTO KristjanESPERANTO marked this pull request as draft September 13, 2025 23:31
@KristjanESPERANTO
Copy link
Contributor Author

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
@jens-maus
Copy link
Owner

@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 test/test-fromURL.js file.

@KristjanESPERANTO
Copy link
Contributor Author

@jens-maus Thanks for the positive feedback 🙂 I think I've solved the problems. Please run the GitHub workflows again.

@jens-maus
Copy link
Owner

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.

@KristjanESPERANTO
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Sep 14, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@KristjanESPERANTO KristjanESPERANTO marked this pull request as ready for review September 14, 2025 11:46
@Apollon77
Copy link
Contributor

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.

Copy link

@coderabbitai coderabbitai bot left a comment

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)
node-ical.d.ts (1)

65-65: Fix callback nullability: data can be null on errors

Runtime behavior (and tests) pass null for data when error is 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 coercion

Prefer assert.strictEqual to 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 consistency

A few route handlers still use an unused request param; standardize on _request like 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 races

Minor ordering tweak: invoke this.callback(...) first, then done(); 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 flakiness

Disable 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

📥 Commits

Reviewing files that changed from the base of the PR and between ebc180c and 64e503f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is 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 inconclusive

Provided 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

@KristjanESPERANTO KristjanESPERANTO marked this pull request as draft September 14, 2025 11:58
@KristjanESPERANTO
Copy link
Contributor Author

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.

are they 100% compatible? ... So in my eyes that could still require a major version bump

I doubt that. And I also would recommend a major version bump.

@jens-maus
Copy link
Owner

I could have tested the workflows in my fork beforehand, sorry. I've done that now and the tests should work.

Only the CodeQL run is still producing an error. Please review.

But next, I'll take a look at the new CodeRabit comments.

Definitly a good idea, yes.

are they 100% compatible? ... So in my eyes that could still require a major version bump

I doubt that. And I also would recommend a major version bump.

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?

@Apollon77
Copy link
Contributor

@Apollon77 Do you know if we are using custom axios option sin ioBroker.ical which is using node-ical?

fromurl() is not used because ioBroker has own logic to fetch the file including caching and such, so issue here, but maybe for others

@KristjanESPERANTO
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Sep 14, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@KristjanESPERANTO KristjanESPERANTO marked this pull request as ready for review September 14, 2025 12:48
@jens-maus
Copy link
Owner

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...

@KristjanESPERANTO
Copy link
Contributor Author

I ran the MagicMirror e2e tests with my fork without an issue. So that side seems fine 🙂

@jens-maus jens-maus merged commit d1006f9 into jens-maus:master Sep 14, 2025
16 checks passed
@KristjanESPERANTO KristjanESPERANTO deleted the fetch branch September 14, 2025 16:23
khassel pushed a commit to MagicMirrorOrg/MagicMirror that referenced this pull request Sep 15, 2025
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 🙂
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.

3 participants