Skip to content

Refactor common fetch logic into baseFetch function#560

Merged
wKovacs64 merged 4 commits intomainfrom
unified-fetch
Nov 30, 2025
Merged

Refactor common fetch logic into baseFetch function#560
wKovacs64 merged 4 commits intomainfrom
unified-fetch

Conversation

@wKovacs64
Copy link
Owner

@wKovacs64 wKovacs64 commented Nov 30, 2025

This pull request refactors the API fetch logic by introducing a shared baseFetch utility, which centralizes request setup, header construction, and URL building. The change reduces code duplication, improves maintainability, and standardizes how requests are made across the haveibeenpwned and pwnedpasswords modules. Additionally, related tests are updated to reflect improved error handling.

API Request Refactoring:

  • Introduced a new baseFetch utility in src/api/base-fetch.ts to handle request construction, headers (including default User-Agent), timeouts, and query parameters in a reusable way.
  • Refactored fetchFromApi in src/api/haveibeenpwned/fetch-from-api.ts and src/api/pwnedpasswords/fetch-from-api.ts to use baseFetch, removing duplicated code for URL and header setup. [1] [2] [3] [4]

Testing and Error Handling:

  • Updated test cases in src/api/haveibeenpwned/__tests__/fetch-from-api.test.ts and src/api/pwnedpasswords/__tests__/fetch-from-api.test.ts to expect the new standardized error message (TypeError: Invalid URL) from the improved URL validation in baseFetch. [1] [2]

Summary by CodeRabbit

  • Refactor
    • Centralized HTTP request handling across APIs for consistent headers, timeouts, and URL/query construction.
  • Tests
    • Relaxed URL-parsing error expectations to be less specific.
  • Chores
    • Updated distribution bundle size limits and added per-file size constraints for builds.

✏️ Tip: You can customize this high-level summary in your review settings.

@changeset-bot
Copy link

changeset-bot bot commented Nov 30, 2025

⚠️ No Changeset found

Latest commit: 824e262

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link

coderabbitai bot commented Nov 30, 2025

Warning

Rate limit exceeded

@wKovacs64 has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 13 minutes and 52 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4906c2c and 824e262.

📒 Files selected for processing (7)
  • .bundlewatch.config.json (1 hunks)
  • src/api/__tests__/base-fetch.test.ts (1 hunks)
  • src/api/base-fetch.ts (1 hunks)
  • src/api/haveibeenpwned/__tests__/fetch-from-api.test.ts (1 hunks)
  • src/api/haveibeenpwned/fetch-from-api.ts (2 hunks)
  • src/api/pwnedpasswords/__tests__/fetch-from-api.test.ts (1 hunks)
  • src/api/pwnedpasswords/fetch-from-api.ts (2 hunks)

Walkthrough

Adds a new typed baseFetch module that centralizes URL, header, and timeout construction; refactors two API modules to use it; updates tests to expect generic URL parsing errors; and updates bundle size configuration entries.

Changes

Cohort / File(s) Summary
New centralized fetch module
src/api/base-fetch.ts
Adds baseFetch export that accepts baseUrl, endpoint, headers, timeoutMs, userAgent, and queryParams. Implements buildUrl (concatenate base + endpoint, normalize slashes, append/encode query params) and buildHeaders (merge headers, set User-Agent from explicit value or package metadata in non-browser contexts). Uses AbortSignal when timeoutMs is provided.
HIBP API refactor
src/api/haveibeenpwned/fetch-from-api.ts
Replaces manual fetch/RequestInit and URL assembly with baseFetch; conditionally adds HIBP-API-Key header when provided; preserves existing response.ok checks and error handling.
Pwned Passwords API refactor
src/api/pwnedpasswords/fetch-from-api.ts
Replaces inlined fetch and AbortSignal timeout with baseFetch; passes Add-Padding header when enabled and mode via queryParams; retains existing BAD_REQUEST and non-ok response handling.
Test updates
src/api/haveibeenpwned/__tests__/fetch-from-api.test.ts,
src/api/pwnedpasswords/__tests__/fetch-from-api.test.ts
Generalizes test expectations for URL parsing/request setup errors to a generic TypeError: Invalid URL.
Bundle size config
.bundlewatch.config.json
Updates pre-bundled ESM size limit and adds multiple Unbundled ESM entries with explicit max sizes.
Manifest referenced
package.json
Referenced by the new module (via generated package-info.js) for package name/version; no API signature changes.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Caller (module using fetchFromApi)
  participant API as fetchFromApi (haveibeenpwned / pwnedpasswords)
  participant Base as baseFetch
  participant Net as fetch / Network
  participant PKG as package-info (optional)

  Caller->>API: invoke fetchFromApi(params)
  API->>Base: call baseFetch(baseUrl, endpoint, headers, timeoutMs, userAgent, queryParams)
  Base->>PKG: (conditionally) read package name/version for User-Agent
  Base->>Net: perform fetch(fullUrl, requestInit with headers + AbortSignal)
  Net-->>Base: response
  Base-->>API: Response
  API-->>Caller: parsed/validated result or thrown error
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect src/api/base-fetch.ts for edge cases: slash normalization, query param encoding, and AbortSignal timeout behavior.
  • Verify header precedence and non-browser User-Agent injection (package-info usage).
  • Confirm fetch-from-api.ts refactors preserve previous error branches and conditional headers.
  • Check updated tests reflect runtime errors across environments.

Possibly related PRs

  • Add latestBreach module #537 — Related because this PR centralizes fetch logic used by modules (e.g., latestBreach and other endpoints) that previously called fetchFromApi.

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 common fetch logic into baseFetch function' accurately and concisely describes the main change: introducing a shared baseFetch utility to centralize common fetch logic across multiple API modules.

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.

@codecov
Copy link

codecov bot commented Nov 30, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (9cb7f0b) to head (824e262).
⚠️ Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #560   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           20        21    +1     
  Lines          131       137    +6     
  Branches        45        45           
=========================================
+ Hits           131       137    +6     
Files with missing lines Coverage Δ
src/api/base-fetch.ts 100.00% <100.00%> (ø)
src/api/haveibeenpwned/fetch-from-api.ts 100.00% <100.00%> (ø)
src/api/pwnedpasswords/fetch-from-api.ts 100.00% <100.00%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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: 0

🧹 Nitpick comments (2)
src/api/base-fetch.ts (2)

29-40: Consider defensive handling for endpoint without leading slash.

If endpoint doesn't start with /, the URL concatenation on line 31 could produce malformed URLs (e.g., https://example.compath instead of https://example.com/path). While current callers appear to always use leading slashes, adding defensive handling would make this utility more robust.

 function buildUrl(baseUrl: string, endpoint: string, queryParams?: Record<string, string>): string {
   const base = baseUrl.replace(/\/$/g, '');
-  const url = new URL(`${base}${endpoint}`);
+  const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
+  const url = new URL(`${base}${normalizedEndpoint}`);

   if (queryParams) {
     for (const [key, value] of Object.entries(queryParams)) {
       url.searchParams.set(key, value);
     }
   }

   return url.toString();
 }

1-2: Consider adding a type declaration for the generated file.

The @ts-ignore works, but a .d.ts file for package-info.js would provide better type safety and IDE support. This is a minor improvement that can be deferred.

You could add src/api/haveibeenpwned/package-info.d.ts:

export declare const PACKAGE_NAME: string;
export declare const PACKAGE_VERSION: string;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 393efed and d196164.

📒 Files selected for processing (5)
  • src/api/base-fetch.ts (1 hunks)
  • src/api/haveibeenpwned/__tests__/fetch-from-api.test.ts (1 hunks)
  • src/api/haveibeenpwned/fetch-from-api.ts (2 hunks)
  • src/api/pwnedpasswords/__tests__/fetch-from-api.test.ts (1 hunks)
  • src/api/pwnedpasswords/fetch-from-api.ts (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/api/pwnedpasswords/fetch-from-api.ts (1)
src/api/base-fetch.ts (1)
  • baseFetch (4-27)
src/api/haveibeenpwned/fetch-from-api.ts (1)
src/api/base-fetch.ts (1)
  • baseFetch (4-27)
🔇 Additional comments (6)
src/api/pwnedpasswords/__tests__/fetch-from-api.test.ts (1)

9-13: LGTM!

The updated error expectation correctly reflects the standardized TypeError: Invalid URL thrown by the URL constructor in baseFetch.buildUrl() when given an invalid base URL. This is cleaner than testing for implementation-specific error messages.

src/api/haveibeenpwned/__tests__/fetch-from-api.test.ts (1)

53-58: LGTM!

Consistent with the pwnedpasswords test update—the standardized TypeError: Invalid URL expectation properly reflects the centralized URL handling in baseFetch.

src/api/pwnedpasswords/fetch-from-api.ts (1)

45-55: Clean refactor to use centralized baseFetch.

The implementation correctly delegates request construction while preserving the original behavior. Header handling is appropriately minimal.

One minor observation: mode is always passed as a query parameter (defaulting to 'sha1'). If the API treats absence of the mode parameter as defaulting to SHA-1, you could consider only including it when explicitly set to 'ntlm' to reduce URL noise. However, being explicit is also a valid choice.

src/api/haveibeenpwned/fetch-from-api.ts (1)

74-83: LGTM!

The refactor cleanly delegates request construction to baseFetch while preserving all existing response handling and error logic. The conditional HIBP-API-Key header is handled appropriately.

src/api/base-fetch.ts (2)

42-52: Well-designed cross-environment User-Agent handling.

The conditional logic correctly handles three scenarios: custom User-Agent provided, Node environment (uses package default), and browser environment (lets browser set its own). This is a good pattern for isomorphic JavaScript.


4-27: LGTM!

The baseFetch function is well-structured with clear parameter types and clean delegation to helper functions. Using AbortSignal.timeout for request timeouts is the modern, recommended approach.

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: 0

🧹 Nitpick comments (2)
src/api/base-fetch.ts (2)

1-2: Prefer @ts-expect-error over @ts-ignore for type suppressions.

TypeScript best practices recommend using @ts-expect-error instead of @ts-ignore because it will error if the suppression becomes unnecessary, helping keep the codebase clean.

Additionally, note that this creates a coupling where the base fetch utility depends on a generated file within the haveibeenpwned subdirectory. This is workable but means the base utility isn't fully independent of specific API modules.

Apply this diff:

-// @ts-ignore - package-info.js is generated
+// @ts-expect-error - package-info.js is generated
 import { PACKAGE_NAME, PACKAGE_VERSION } from './haveibeenpwned/package-info.js';

29-41: Minor: Remove unnecessary global flag from regex.

The regular expression /\/$/g uses the global flag g, but since it only matches at the end of the string (due to the $ anchor), the global flag is unnecessary. Using /\/$/ would be equivalent and slightly more precise.

Apply this diff:

-  const base = baseUrl.replace(/\/$/g, '');
+  const base = baseUrl.replace(/\/$/, '');
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 03be6a1 and 4906c2c.

📒 Files selected for processing (1)
  • src/api/base-fetch.ts (1 hunks)
🔇 Additional comments (2)
src/api/base-fetch.ts (2)

43-53: LGTM!

The header building logic is clean and handles the User-Agent appropriately:

  • Explicit userAgent parameter takes precedence
  • Falls back to package name/version in non-browser environments
  • Allows browsers to use their default User-Agent

4-27: No compatibility issue with AbortSignal.timeout() for the target environment.

The project targets Node.js >= 20.19.0 (per package.json), which fully supports AbortSignal.timeout() (available since v17.3.0). No polyfill or compatibility check is needed.

Likely an incorrect or invalid review comment.

@wKovacs64 wKovacs64 merged commit 40ba51b into main Nov 30, 2025
17 checks passed
@wKovacs64 wKovacs64 deleted the unified-fetch branch November 30, 2025 19:32
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