DEV Community

LubuSeb
LubuSeb

Posted on

Fixing npmx's 502 for packages blocked by jsDelivr's 150 MB limit

Summer Bug Smash: Clear the Lineup 🐛🛹

This is a submission for DEV's Summer Bug Smash: Clear the Lineup powered by Sentry.

Project Overview

npmx.dev is a fast, modern browser for the npm registry. It lets developers browse package source files, compare versions, and inspect package information without leaving the browser.

I worked on npmx issue #2899. Opening the code or skills view for next@16.2.9 returned a 502 because npmx could not retrieve the package file list from jsDelivr.

The issue reporter, 314systems, had already done the important initial diagnosis. They identified that the package exceeded jsDelivr's configured 150 MB limit. I started from that finding, confirmed the 403 response, reproduced how it became a 502 inside npmx, and implemented a bounded fallback.

I am not claiming that Sentry discovered the cause. I used Sentry later to independently verify the runtime boundary and the behavior before and after the fix.

The issue was opened before the challenge, but the implementation and evidence work were completed during the contest period. The core fix and both final Sentry evidence-state commits are timestamped August 9, 2026.

Bug Fix or Performance Improvement

Before the fix, npmx had one provider for package file metadata and content: jsDelivr.

For an oversized package such as next@16.2.9, the sequence was:

  1. npmx requested package metadata from jsDelivr.
  2. jsDelivr returned 403.
  3. npmx converted that upstream failure into a 502.
  4. The code browser and related views stopped there.

I reproduced this against a production build from the baseline commit, 5be120b.

Production-build probe Baseline result
File tree for next@16.2.9 502
package.json for next@16.2.9 502
File tree for a normal vue version 200

Before the fix, the next@16.2.9 code page shows

The fix keeps jsDelivr as the primary provider. If, and only if, jsDelivr returns 403, npmx retries the same metadata or file request through UNPKG.

After the fix:

Production-build probe Fixed result
File tree for next@16.2.9 200, with 8,076 files
package.json for next@16.2.9 200
File tree for the same vue control 200 through jsDelivr, with no fallback

After the fix, the next@16.2.9 code page loads its full package file tree

This is a functional fix, not a performance claim. The result is that a package refused by jsDelivr can still be browsed instead of producing a 502.

Code

The central provider decision is deliberately narrow:

async function fetchWithFallback(
  primaryUrl: string,
  fallbackUrl: string,
  signal?: AbortSignal,
): Promise<PackageFetchResult> {
  const primary = await fetch(primaryUrl, { signal })

  if (primary.status !== 403) {
    if (!primary.ok) await cancelResponseBody(primary)
    return { provider: 'jsdelivr', response: primary }
  }

  await cancelResponseBody(primary)

  const fallback = await fetch(fallbackUrl, { signal })
  if (!fallback.ok) await cancelResponseBody(fallback)

  return { provider: 'unpkg', response: fallback }
}
Enter fullscreen mode Exit fullscreen mode

A 404, 500, or successful response from jsDelivr keeps its existing behavior. This is not a general retry for every failure.

UNPKG and jsDelivr return different metadata shapes, so changing the URL was only part of the work. jsDelivr provides a nested tree. UNPKG provides a flat file list. The fix validates the UNPKG response and converts it into the tree structure that the rest of npmx expects.

if (provider === 'unpkg') {
  const parsed = v.safeParse(UnpkgMetadataResponseSchema, payload)

  if (!parsed.success || parsed.output.package !== packageName) {
    throw invalidFileListError()
  }

  return {
    package: packageName,
    version,
    tree: convertUnpkgToFileTree(parsed.output.files),
  }
}
Enter fullscreen mode Exit fullscreen mode

The core fix touches 18 files: 12 application or shared-code files and 6 test or fixture files. That breadth is necessary because the jsDelivr assumption was spread across several consumers:

  • File-tree metadata
  • Individual file content
  • Package comparisons
  • File comparisons
  • Skills processing
  • Raw-file links in the code viewer
  • Shared provider types and URL construction
  • Tests and route fixtures for those paths

A narrower patch to the visible package page would have left other routes failing on the same package.

The Sentry instrumentation is separate from the upstream fix. It exists only in evidence commits on my fork so I could compare controlled production builds without asking the npmx maintainers to accept challenge-specific monitoring code.

My Improvements

One fallback policy for every package-file consumer

I added shared helpers for package metadata and file requests. The code viewer, file tree, comparison routes, and skills path now follow the same provider policy.

Normal traffic still uses jsDelivr. Only a primary 403 activates UNPKG.

Strict metadata validation

The UNPKG response is treated as external input. The converter validates paths, sizes, integrity values, and the requested package name before creating the internal tree.

It also rejects duplicate paths and file-directory conflicts instead of silently accepting an ambiguous result.

Fixed processing limits

Supporting larger packages does not mean accepting unlimited metadata or file content.

The UNPKG metadata path has these fixed limits:

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

File bodies use a bounded streaming reader. The code viewer still limits highlighted source files to 500 KiB, while its package.json helper allows up to 2 MiB.

Those are intentional limits. A package with metadata beyond the new bounds will still fail safely, and a source file larger than the existing viewer limit will still not be rendered inline. The fix resolves the provider-level package failure. It does not remove every content-size restriction in npmx.

Fixed and encoded provider URLs

Package versions and individual path segments are encoded before being added to provider URLs. Provider origins are fixed in code rather than accepted from request input.

Raw-file viewer tradeoff

The raw-file buttons now link to UNPKG's file viewer. This makes raw access work for a package that jsDelivr refuses to serve.

The tradeoff is that the raw button uses UNPKG even when the in-app request succeeded through jsDelivr. The current API response does not expose the selected provider to the UI, so the link cannot switch dynamically without widening the public response shape.

I chose working raw access for oversized packages over keeping the raw link visually tied to the primary provider. This does add an UNPKG dependency to the raw-view action, but it does not change the primary server-fetch path for normal packages.

Regression coverage

The tests cover:

  • jsDelivr success without fallback
  • Fallback after a jsDelivr 403
  • File and metadata requests
  • Invalid UNPKG payloads
  • Duplicate paths and file-directory conflicts
  • Metadata, file-count, depth, and complexity limits
  • Bounded response reading
  • Scoped packages and encoded file paths
  • Existing normal-package behavior

The final core-fix validation included:

Check Result
Unit suite 1,743 passing
Type checking Passing
Linting Passing
Production build Passing
Focused package-code page tests Passing
Live next@16.2.9 production probe 200, 8,076 files
Normal vue control 200, no fallback
Browser check of the package-code page Passing
Upstream pull request checks All passing

Best Use of Sentry

I used Sentry Error Monitoring and custom server tracing to verify the runtime behavior of the existing failure and the fallback.

Again, Sentry did not discover jsDelivr's 150 MB limit. The reporter, 314systems, identified that cause. Sentry let me independently show what npmx did with the upstream response and whether the fix changed only the intended path.

Controlled before-and-after evidence

I instrumented two fork-only production states:

  • Baseline source: 5be120b
  • Baseline evidence state: bf0e0465
  • Fixed source: 941bf209
  • Final upstream PR head: 098a59b6
  • Fixed evidence state: c95e027e
  • Baseline release: npmx-2899-before-5be120b-clean
  • Fixed release: npmx-2899-after-941bf209-clean
  • Environment: bugsmash-local

Both states use @sentry/nuxt in production-build mode. Instrumentation is enabled only when BUGSMASH_SENTRY_ENABLED=true, and the DSN remains in an environment variable.

The hosted fixed-state evidence uses the core implementation at 941bf209. The later 098a59b6 review follow-up does not change the provider fallback. It preserves comparison aborts, clarifies size-limit errors, expands one comment, and adds a bounded-reader test.

I added custom server spans around the provider boundary. They record a small set of explicit attributes:

{
  'asset.kind': assetKind,
  'cdn.provider': provider,
  'cdn.role': role,
  'fallback.used': role === 'fallback',
  'npm.package': packageName,
  'npm.version': version,
  'source.commit': sourceCommit,
  'bugsmash.synthetic': true,
}
Enter fullscreen mode Exit fullscreen mode

The provider span also records the HTTP status and whether a primary 403 triggered the fallback.

This is custom server tracing within one synthetic local service. I did not propagate traces to jsDelivr or UNPKG, so I am not describing it as distributed tracing.

What Error Monitoring showed before the fix

The baseline production request returned 502.

The local Sentry envelope capture contained:

  • The npmx error event
  • A custom jsDelivr provider transaction
  • HTTP status 403
  • Sentry status permission_denied
  • fallback.available: false
  • Matching trace IDs between the error event and provider transaction

Sentry baseline error event for the failed next@16.2.9 file-tree request

  • Baseline hosted tree trace ID: 4de5891fa25248b58cd01be1f9a35975
  • Baseline hosted tree error event: 596c2d3547914b619f661223aa7e8eda
  • Baseline hosted file trace ID: 6a06a704a73148fba774f0ba87ee0797

What custom server tracing showed after the fix

The same production request returned 200 after the fix.

The local evidence run showed this sequence:

  1. The jsDelivr primary metadata request returned 403.
  2. The custom span recorded fallback.triggered: true.
  3. The UNPKG fallback returned 200.
  4. npmx converted the metadata into a tree containing 8,076 files.
  5. The API request completed successfully.

The package-file route showed the same recovery for package.json.

The vue control stayed on jsDelivr and recorded no fallback. That control matters because it shows the fix did not redirect normal package traffic.

Sentry fixed-state trace showing the primary jsDelivr request returning 403

Sentry fixed-state trace showing the UNPKG fallback returning 200

Sentry conversion span showing a successful tree containing 8,076 files

  • Fixed hosted tree trace ID: 564054b21cbb43c7b195cf3e08a17ba6
  • Fixed hosted file trace ID: 113024125b8641ed9dfecee823b33550

Sentry Vue control trace showing jsDelivr returning 200 without a fallback

  • Control hosted trace ID: aaf98132b1014b6188ce8c30f22a04ea

Keeping the synthetic evidence small and private

These were synthetic local requests, but I still configured the SDK to collect as little request data as possible.

The evidence setup disables user information, cookies, request and response headers, bodies, query parameters, breadcrumbs, source maps, database query data, frame variables, and outgoing trace propagation. Request objects are removed before events are sent, and the custom evidence spans are marked as synthetic.

Sentry still derived coarse location labels on some child spans from the network ingress. The private dashboard retained those labels even though IP storage was disabled, so I kept the dashboard private and excluded those fields from every publishable screenshot.

I also scanned the captured raw envelopes. They did not contain local user paths, authorization values, cookies, user-agent values, IP addresses, or request and response headers.

Sentry's value here was verification. Error Monitoring preserved the baseline failure. Custom server tracing made the provider decision visible. Together they showed one failed provider becoming a controlled fallback and a successful response, while the normal control remained on the original path.

Top comments (0)