Skip to content

fix: fall back to unpkg for oversized packages - #3161

Open
LubuSeb wants to merge 2 commits into
npmx-dev:mainfrom
LubuSeb:codex/fix-large-package-cdn-fallback
Open

fix: fall back to unpkg for oversized packages#3161
LubuSeb wants to merge 2 commits into
npmx-dev:mainfrom
LubuSeb:codex/fix-large-package-cdn-fallback

Conversation

@LubuSeb

@LubuSeb LubuSeb commented Aug 9, 2026

Copy link
Copy Markdown

🔗 Linked issue

Fixes #2899

🧭 Context

next@16.2.9 is larger than jsDelivr's 150 MB package limit. Its metadata and file requests return 403, which npmx currently surfaces as 502.

I kept jsDelivr as the primary provider. When it returns 403, npmx now retries the same package metadata or file request through UNPKG. Other statuses keep their existing behavior.

📚 Description

UNPKG returns a flat metadata list, so the fallback validates and converts it into the file tree npmx already expects. The shared provider logic covers file trees, individual files, comparisons and skills processing.

The fallback is bounded rather than open-ended:

  • 10 MiB maximum metadata response
  • 50,000 files
  • 100 path segments per file
  • 250,000 total path segments
  • bounded readers for file bodies

Package versions and file path segments are encoded before they are added to provider URLs. Raw-file actions use UNPKG's file viewer so they still work when jsDelivr rejects the whole package. The current API response does not expose the selected provider, so the viewer link cannot switch dynamically without widening that response.

What I checked:

  • next@16.2.9 file tree: 502 before, 200 after with 8,076 files
  • next@16.2.9/package.json: 502 before, 200 after
  • vue@3.5.28 control: 200 through jsDelivr with no fallback
  • 1,743 unit tests
  • type checking
  • linting
  • production build

Before

After

@vercel

vercel Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
npmx.dev Ready Ready Preview Aug 9, 2026 10:18am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
docs.npmx.dev Ignored Ignored Preview Aug 9, 2026 10:18am
npmx-lunaria Ignored Ignored Aug 9, 2026 10:18am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added automatic fallback to an alternative package CDN when primary package data is unavailable.
    • Improved package file browsing and raw-file links across supported package states.
    • Added validation and limits for package metadata, file sizes, file counts, and path complexity.
    • Limited skill discovery to 100 entries with safer processing.
  • Bug Fixes

    • Improved handling of oversized, malformed, unavailable, and failed package responses.
    • Corrected URL encoding for scoped packages, versions, and file paths.

Walkthrough

Package file access now supports jsDelivr and unpkg fallback, bounded response reads, validated metadata, and shared URL construction. Registry, file-tree, skill, and package-code routes use these utilities.

Changes

Package file retrieval

Layer / File(s) Summary
Shared package contracts and URLs
shared/types/npm-registry.ts, shared/utils/package-files.ts, test/unit/shared/utils/package-files.spec.ts
Adds provider metadata types and URL builders for package files, metadata, and unpkg viewer pages.
Provider fallback and bounded responses
server/utils/package-files.ts, test/unit/server/utils/package-files.spec.ts
Adds jsDelivr-to-unpkg fallback, streamed response limits, cancellation, UTF-8 decoding, and size-limit errors.
Validated metadata and registry routes
server/utils/file-tree.ts, server/api/registry/..., modules/runtime/server/cache.ts, test/unit/server/utils/file-tree.spec.ts, test/fixtures/mock-routes.cjs
Validates provider metadata, converts unpkg listings, and applies shared fetching and response limits to registry routes.
Skills retrieval limits and concurrency
server/utils/skills.ts, test/unit/server/utils/skills.spec.ts
Uses shared package fetching, limits discovery to 100 skills, and processes skill entries with bounded concurrency.
Package viewer link integration
app/components/Code/Header.vue, app/pages/package-code/..., test/nuxt/pages/PackageCodePage.spec.ts
Uses unpkg viewer URLs for raw-file links in package-code views and updates the expected fallback URL.

Sequence Diagram(s)

sequenceDiagram
  participant PackageCodePage
  participant RegistryRoute
  participant fetchPackageFile
  participant jsDelivr
  participant unpkg
  PackageCodePage->>RegistryRoute: request package file
  RegistryRoute->>fetchPackageFile: fetch package file
  fetchPackageFile->>jsDelivr: request primary URL
  jsDelivr-->>fetchPackageFile: 403 response
  fetchPackageFile->>unpkg: request fallback URL
  unpkg-->>fetchPackageFile: bounded package response
  fetchPackageFile-->>RegistryRoute: provider and response
  RegistryRoute-->>PackageCodePage: file content or error
Loading

Possibly related PRs

Suggested reviewers: ghostdevv

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #2899 by allowing oversized packages to load through UNPKG when jsDelivr returns HTTP 403.
Out of Scope Changes check ✅ Passed The changes remain within the stated fallback, validation, URL handling, size-limit, file, comparison, and skills objectives.
Title check ✅ Passed The title clearly summarises the main change: using UNPKG as a fallback for oversized packages.
Description check ✅ Passed The description directly explains the UNPKG fallback, limits, affected areas, and validation results.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Hello! Thank you for opening your first PR to npmx, @LubuSeb! 🚀

Here’s what will happen next:

  1. Our GitHub bots will run to check your changes.
    If they spot any issues you will see some error messages on this PR.
    Don’t hesitate to ask any questions if you’re not sure what these mean!

  2. In a few minutes, you’ll be able to see a preview of your changes on Vercel

  3. One or more of our maintainers will take a look and may ask you to make changes.
    We try to be responsive, but don’t worry if this takes a few days.

@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

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

Actionable comments posted: 5

🧹 Nitpick comments (7)
server/utils/skills.ts (1)

122-123: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove or expand this comment.

The comment only repeats the function purpose. It does not explain complex or non-obvious logic. Remove it, or document the provider fallback and bounded-read behaviour.

As per coding guidelines, “Add comments only to explain complex logic or non-obvious implementations.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/utils/skills.ts` around lines 122 - 123, Update the comment above the
package-content fetch function by either removing it or expanding it to document
the provider fallback and bounded-read behavior; do not retain the current
generic purpose-only wording.

Source: Coding guidelines

server/api/registry/compare/[...pkg].get.ts (1)

17-26: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

The catch block hides aborts and size errors.

Line 24 converts every failure into null. The caller cannot distinguish a missing package.json from a timeout or from an oversized response. The fallback adds a second sequential request, so aborts are now more likely on this path. Re-throw AbortError and let the caller report the timeout.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/registry/compare/`[...pkg].get.ts around lines 17 - 26, Update the
catch block surrounding fetchPackageFile and readPackageResponseText to re-throw
AbortError instances, while continuing to return null for other failures.
Preserve the existing response and JSON parsing behavior so missing or invalid
package metadata still uses the null fallback.
test/unit/server/utils/package-files.spec.ts (1)

125-147: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add coverage for the null-body branch.

readPackageResponseText has a separate branch for response.body === null at server/utils/package-files.ts Lines 35-40. The current tests never reach it, because every fixture has a body. Add one case with a null body and an oversized text payload to lock the Buffer.byteLength check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/server/utils/package-files.spec.ts` around lines 125 - 147, Add a
test in the readPackageResponseText suite using a Response fixture whose body is
null and whose text payload exceeds the byte limit, then assert rejection with
PackageResponseTooLargeError and the expected Buffer.byteLength-based sizeBytes
value. Ensure this exercises the response.body === null branch without changing
the existing tests.
server/utils/package-files.ts (2)

72-88: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Pass the provider identifiers instead of hardcoding them.

fetchWithFallback receives generic URLs but returns the literals 'jsdelivr' and 'unpkg'. The returned provider and the supplied URL can drift if a caller ever changes the primary provider. Accept the two providers as parameters and derive the URLs inside the helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/utils/package-files.ts` around lines 72 - 88, Update fetchWithFallback
to accept the primary and fallback provider identifiers instead of generic URLs,
derive each provider URL inside the helper, and return those parameters in the
provider fields. Replace the hardcoded 'jsdelivr' and 'unpkg' values while
preserving the existing 403 fallback and response-body cancellation behavior.

72-88: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a default timeout for the provider requests.

fetchWithFallback makes up to two sequential fetch calls. The signal parameter is optional. server/api/registry/file/[...pkg].get.ts calls fetchPackageFile without a signal at Line 46 and Line 62, so a slow CDN can hold the request until the platform limit. The fallback path doubles the worst-case latency compared with the previous single jsDelivr request.

Add a default timeout when the caller supplies no signal, or require a signal from every caller.

♻️ Example: apply a default timeout
+const DEFAULT_PACKAGE_FETCH_TIMEOUT_MS = 10_000
+
 async function fetchWithFallback(
   primaryUrl: string,
   fallbackUrl: string,
   signal?: AbortSignal,
 ): Promise<PackageFetchResult> {
-  const primary = await fetch(primaryUrl, { signal })
+  const requestSignal = signal ?? AbortSignal.timeout(DEFAULT_PACKAGE_FETCH_TIMEOUT_MS)
+  const primary = await fetch(primaryUrl, { signal: requestSignal })
   if (primary.status !== 403) {
     if (!primary.ok) await cancelResponseBody(primary)
     return { provider: 'jsdelivr', response: primary }
   }
 
   await cancelResponseBody(primary)
 
-  const fallback = await fetch(fallbackUrl, { signal })
+  const fallback = await fetch(fallbackUrl, { signal: requestSignal })
   if (!fallback.ok) await cancelResponseBody(fallback)
   return { provider: 'unpkg', response: fallback }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/utils/package-files.ts` around lines 72 - 88, Update fetchWithFallback
so provider fetches cannot run indefinitely when signal is omitted: create and
use a default timeout signal for both sequential fetch calls, while preserving
the caller-provided signal when present. Ensure the timeout resource is cleaned
up after the primary/fallback flow completes.
test/unit/server/utils/file-tree.spec.ts (1)

136-160: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Move the global cleanup into afterEach.

Every new test wraps its assertions in try/finally only to call vi.unstubAllGlobals(). A single afterEach(() => vi.unstubAllGlobals()) in this describe block removes the repetition and keeps each test focused on its assertions. test/unit/server/utils/package-files.spec.ts already uses that pattern at Lines 9-11.

Also applies to: 207-375

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unit/server/utils/file-tree.spec.ts` around lines 136 - 160, Move
vi.unstubAllGlobals() cleanup from the repeated try/finally blocks in the
getPackageFileTree tests into a single afterEach hook within the describe block.
Remove the per-test finally wrappers while preserving each test’s existing
assertions and fetchMock checks.
server/api/registry/file/[...pkg].get.ts (1)

44-52: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Two identical fetchPackageJson helpers now exist. Both routes define the same helper with the same 2 MiB constant and the same catch { return null }. The duplication grew with this PR, because both now call the shared fetcher and reader.

  • server/api/registry/file/[...pkg].get.ts#L44-L52: move this helper and MAX_PACKAGE_JSON_SIZE into server/utils/package-files.ts and import it here.
  • server/api/registry/compare/[...pkg].get.ts#L17-L26: import the shared helper and delete the local copy and constant.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/registry/file/`[...pkg].get.ts around lines 44 - 52, Move
fetchPackageJson and MAX_PACKAGE_JSON_SIZE from
server/api/registry/file/[...pkg].get.ts:44-52 into
server/utils/package-files.ts, export them, and import the shared helper here.
In server/api/registry/compare/[...pkg].get.ts:17-26, import the shared helper
and remove its local fetchPackageJson implementation and constant.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/api/registry/file/`[...pkg].get.ts:
- Around line 80-85: Update the error message in the
PackageResponseTooLargeError handler to report both the actual file size and
MAX_FILE_SIZE using the same unit, preserving the existing formatting and 413
response.

In `@server/utils/file-tree.ts`:
- Around line 185-190: Update the non-OK response handling to map a 404 from
either the jsDelivr or fallback provider to the existing “Package or version not
found” 404 error. Preserve invalidFileListError() for all other response
statuses.
- Around line 114-121: Normalize the hash assigned in the PackageFileTree
construction within the file-tree builder so UNPKG SRI values are converted from
base64 to the same hex SHA-256 encoding returned by jsDelivr before exposing
hash. Preserve existing duplicate-path handling and file metadata, and ensure
equivalent digests compare identically across providers.
- Around line 45-60: The Unpkg metadata validation and conversion flow must
tolerate directory entries and unsupported integrity prefixes. Update the
metadata handling used by getPackageFileTree to filter out non-file/listing
entries before safeParse, and update convertUnpkgToFileTree to skip or
explicitly reject entries whose integrity does not use the supported prefix
instead of blindly slicing sha256-; preserve valid file conversion and
oversized-package handling.

In `@shared/utils/package-files.ts`:
- Around line 15-35: Validate packageName at the start of both getPackageFileUrl
and getPackageMetadataUrl before constructing packageSpec, rejecting values
containing path traversal or URL-delimiter characters such as .., ?, or #.
Ensure invalid names cannot reach PACKAGE_FILE_BASE_URLS or the metadata CDN
URLs, while preserving valid package-name behavior.

---

Nitpick comments:
In `@server/api/registry/compare/`[...pkg].get.ts:
- Around line 17-26: Update the catch block surrounding fetchPackageFile and
readPackageResponseText to re-throw AbortError instances, while continuing to
return null for other failures. Preserve the existing response and JSON parsing
behavior so missing or invalid package metadata still uses the null fallback.

In `@server/api/registry/file/`[...pkg].get.ts:
- Around line 44-52: Move fetchPackageJson and MAX_PACKAGE_JSON_SIZE from
server/api/registry/file/[...pkg].get.ts:44-52 into
server/utils/package-files.ts, export them, and import the shared helper here.
In server/api/registry/compare/[...pkg].get.ts:17-26, import the shared helper
and remove its local fetchPackageJson implementation and constant.

In `@server/utils/package-files.ts`:
- Around line 72-88: Update fetchWithFallback to accept the primary and fallback
provider identifiers instead of generic URLs, derive each provider URL inside
the helper, and return those parameters in the provider fields. Replace the
hardcoded 'jsdelivr' and 'unpkg' values while preserving the existing 403
fallback and response-body cancellation behavior.
- Around line 72-88: Update fetchWithFallback so provider fetches cannot run
indefinitely when signal is omitted: create and use a default timeout signal for
both sequential fetch calls, while preserving the caller-provided signal when
present. Ensure the timeout resource is cleaned up after the primary/fallback
flow completes.

In `@server/utils/skills.ts`:
- Around line 122-123: Update the comment above the package-content fetch
function by either removing it or expanding it to document the provider fallback
and bounded-read behavior; do not retain the current generic purpose-only
wording.

In `@test/unit/server/utils/file-tree.spec.ts`:
- Around line 136-160: Move vi.unstubAllGlobals() cleanup from the repeated
try/finally blocks in the getPackageFileTree tests into a single afterEach hook
within the describe block. Remove the per-test finally wrappers while preserving
each test’s existing assertions and fetchMock checks.

In `@test/unit/server/utils/package-files.spec.ts`:
- Around line 125-147: Add a test in the readPackageResponseText suite using a
Response fixture whose body is null and whose text payload exceeds the byte
limit, then assert rejection with PackageResponseTooLargeError and the expected
Buffer.byteLength-based sizeBytes value. Ensure this exercises the response.body
=== null branch without changing the existing tests.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f7d7fe28-d9ec-4dbb-925c-ef12ed13a4f4

📥 Commits

Reviewing files that changed from the base of the PR and between 5be120b and 941bf20.

📒 Files selected for processing (18)
  • app/components/Code/Header.vue
  • app/pages/package-code/[[org]]/[packageName]/v/[version]/[...filePath].vue
  • modules/runtime/server/cache.ts
  • server/api/registry/compare-file/[...pkg].get.ts
  • server/api/registry/compare/[...pkg].get.ts
  • server/api/registry/file/[...pkg].get.ts
  • server/api/registry/files/[...pkg].get.ts
  • server/utils/file-tree.ts
  • server/utils/package-files.ts
  • server/utils/skills.ts
  • shared/types/npm-registry.ts
  • shared/utils/package-files.ts
  • test/fixtures/mock-routes.cjs
  • test/nuxt/pages/PackageCodePage.spec.ts
  • test/unit/server/utils/file-tree.spec.ts
  • test/unit/server/utils/package-files.spec.ts
  • test/unit/server/utils/skills.spec.ts
  • test/unit/shared/utils/package-files.spec.ts

Comment thread server/api/registry/file/[...pkg].get.ts
Comment thread server/utils/file-tree.ts
Comment thread server/utils/file-tree.ts
Comment thread server/utils/file-tree.ts
Comment thread shared/utils/package-files.ts
@LubuSeb
LubuSeb force-pushed the codex/fix-large-package-cdn-fallback branch from 908530c to 098a59b Compare August 9, 2026 10:15
@serhalp serhalp added needs review This PR is waiting for a review from a maintainer back Server, Data p2 Priority 2 bug (medium) labels Aug 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

back Server, Data needs review This PR is waiting for a review from a maintainer p2 Priority 2 bug (medium)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] 502 Error: Failed to fetch file list from jsDelivr (Package size exceeds 150MB limit)

2 participants