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:
- npmx requested package metadata from jsDelivr.
- jsDelivr returned 403.
- npmx converted that upstream failure into a 502.
- 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 |
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 |
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
- Issue: npmx-dev/npmx.dev#2899
- Core fix: 941bf209
- Review follow-up: 098a59b6
- Upstream pull request: npmx-dev/npmx.dev#3161
- Baseline Sentry evidence state: bf0e0465
- Fixed Sentry evidence state: c95e027e
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 }
}
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),
}
}
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,
}
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
-
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:
- The jsDelivr primary metadata request returned 403.
- The custom span recorded
fallback.triggered: true. - The UNPKG fallback returned 200.
- npmx converted the metadata into a tree containing 8,076 files.
- 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.
-
Fixed hosted tree trace ID:
564054b21cbb43c7b195cf3e08a17ba6 -
Fixed hosted file trace ID:
113024125b8641ed9dfecee823b33550
-
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)