Skip to content

fix: add null guards, NaN checks, and try-catch to prevent runtime crashes#348

Merged
LautaroPetaccio merged 5 commits intomainfrom
fix/crash-guards-null-safety
Apr 14, 2026
Merged

fix: add null guards, NaN checks, and try-catch to prevent runtime crashes#348
LautaroPetaccio merged 5 commits intomainfrom
fix/crash-guards-null-safety

Conversation

@LautaroPetaccio
Copy link
Copy Markdown
Contributor

Summary

Several code paths in the validator can crash with unhandled exceptions instead of returning proper validation errors. This PR adds defensive guards so the validator fails gracefully with clear error messages rather than throwing.

Why these changes are needed

  • entity-structure.ts: The null/empty check on entity.pointers runs after new Set(entity.pointers), which throws a TypeError if pointers is null. The null check is unreachable in the crash case. Reordering puts the null check first so it catches the problem before the Set constructor runs.

  • timestamps.ts: All 10 ADR timestamp constants are overridable via environment variables using parseInt(), but none validate the result. If an env var is set to a non-numeric value (e.g. "invalid"), parseInt returns NaN, and since NaN >= anyNumber is always false, the corresponding validateAfterADRxx gate silently disables the wrapped validator. A single misconfigured env var like ADR_45_TIMESTAMP=invalid would disable IPFS hashing, metadata schema, thumbnail, and content validation — with no log message. A shared parseTimestamp helper now falls back to the default when the parsed value is NaN. The incorrect timestamp number in the ADR-74 comment is also fixed.

  • scene.ts: metadata?.display.navmapThumbnail uses optional chaining on metadata but not on display. If metadata exists but display is undefined, this throws. Similarly, entity.content.some(...) throws if content is undefined. Added optional chaining on both.

  • profile.ts: entity.content.map(...) at two call sites throws if content is undefined. Other locations in the same file already guard with entity.content ?? []; these two were missed. Also, parseUrn() from @dcl/urn-resolver is async and can throw on malformed input, but the wearable and emote URN validators call it without try-catch. If it throws instead of returning null, the entire validator crashes. Added try-catch that treats thrown exceptions the same as unresolvable URNs.

  • outfits.ts and utils.ts: Same parseUrn throw risk — wrapped in try-catch.

  • subgraph/scenes.ts: The scene access validator accesses estate.owners[0].address and parcel.owners[0].address without checking if the owners array has elements. The subgraph queries filter by createdAt_lte: $timestamp with first: 1, so if no ownership record exists at the queried timestamp, the array is empty and [0].address throws a TypeError. This crashes into the retry() loop (5 retries, 100ms each), then propagates as an internal error. The fix checks owners.length first and returns false (no access) when empty. Also adds isNaN validation after parseInt on pointer coordinates so "abc,def" produces a clear "invalid pointer" error instead of passing NaN to GraphQL queries. The typo "state" in an error message is corrected to "estate".

  • on-chain/scenes.ts: Same isNaN guard added for pointer coordinate parsing.

Test plan

  • npm run build compiles cleanly
  • npm test — all 325 existing tests pass
  • npm run lint:check — no errors

🤖 Generated with Claude Code

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 14, 2026

Test this pull request

  • The package can be tested by running
    yarn upgrade "https://sdk-team-cdn.decentraland.org/@dcl/content-validator/branch/fix/crash-guards-null-safety/dcl-content-validator-7.1.1-24416111545.commit-ef5b5b5.tgz"

@coveralls
Copy link
Copy Markdown

coveralls commented Apr 14, 2026

Coverage Status

coverage: 83.062% (+1.4%) from 81.689% — fix/crash-guards-null-safety into main

…ashes

- Reorder entity-structure.ts pointer checks so null check runs before Set constructor
- Add NaN-safe parseInt helper for all ADR timestamp env vars in timestamps.ts
- Fix comment with wrong timestamp value for ADR_74_TIMESTAMP
- Add optional chaining on scene.ts metadata.display and entity.content
- Add null guards on profile.ts entity.content access
- Wrap parseUrn calls in try-catch in profile.ts, outfits.ts, and utils.ts
- Guard owners[0] access in subgraph scenes.ts for empty arrays
- Add NaN validation after parseInt on scene pointer coordinates
- Fix typo "state" -> "estate" in error message
- entity-structure: validate null/undefined pointers fail gracefully
- scenes: validate non-numeric coordinates produce clear error messages
- scenes: validate undefined content and display don't throw
- profiles: validate undefined entity.content returns error not crash
@LautaroPetaccio LautaroPetaccio force-pushed the fix/crash-guards-null-safety branch from a147db4 to 77789f0 Compare April 14, 2026 17:19
Copy link
Copy Markdown

@decentraland-bot decentraland-bot left a comment

Choose a reason for hiding this comment

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

Review: PR #348 — Null Guards, NaN Checks, and Try-Catch for Runtime Crashes

Verdict: ✅ APPROVE

Solid defensive hardening across the codebase. All changes are correct and prevent real crash scenarios.

Key Changes Reviewed

File Fix Assessment
entity-structure.ts Null check before new Set() ✅ Correct — prevents TypeError on null/undefined pointers
timestamps.ts parseTimestamp helper with NaN fallback ✅ Correct — prevents silent validator disabling via misconfigured env vars
scene.ts Optional chaining on display?.navmapThumbnail + content ?? [] ✅ Correct
profile.ts parseUrn try-catch + content ?? [] guards ✅ Correct
outfits.ts / utils.ts parseUrn try-catch ✅ Correct
subgraph/scenes.ts Empty owners guard + NaN coord check + "state"→"estate" typo ✅ Correct
on-chain/scenes.ts NaN coordinate check ✅ Correct

Findings

P2 — Important:

  1. [Pattern] The parseUrn try-catch is repeated identically in 4 locations (profile.ts ×2, outfits.ts, utils.ts). Consider extracting a safeParseUrn() utility in utils.ts to centralize this pattern. This would also align with the existing parseUrnNoFail functions found elsewhere in the codebase.

P3 — Nice-to-Have:

  1. [Testing] No tests cover: (a) parseTimestamp with NaN input, (b) parseUrn throwing (vs returning null), (c) empty owners array in subgraph scenes. While these are defensive paths, adding at least one test per guard would prevent future regressions.

  2. [Testing] In subgraph/scenes.ts, when NaN is detected the code calls controller.abort() and break, but already-queued PQueue tasks may still execute. Consider calling queue.clear() after controller.abort() to prevent unnecessary subgraph queries.

  3. [Minor] SCENE_VALIDATIONS_CONCURRENCY parsing (line 55 in subgraph/scenes.ts) could also benefit from the same parseTimestamp-style NaN guard.

CI Status

All checks passing. Coverage increased +0.8%.

Requested by Lautaro Petaccio via Slack

… tests

Address review feedback:

- Extract safeParseUrn() in utils.ts to replace 4 identical inline
  try-catch blocks in profile.ts, outfits.ts, and utils.ts. Aligns
  with the existing parseUrnNoFail pattern used in stores.ts and items.ts.

- Add NaN and non-positive guard to SCENE_VALIDATIONS_CONCURRENCY parsing,
  falling back to 10 if the env var is invalid or <= 0.

- Call queue.clear() after controller.abort() when NaN coordinates are
  detected, so already-queued tasks don't run unnecessary subgraph queries.

- Add tests for safeParseUrn (valid, unresolvable, malformed input).

- Add tests for parseTimestamp (valid env var, invalid env var, empty string).
Copy link
Copy Markdown
Contributor

@braianj braianj left a comment

Choose a reason for hiding this comment

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

LGTM

Comment on lines +313 to +316
if (parcel.owners.length > 0) {
if (
await hasAccessThroughAuthorizations(parcel.owners[0].address, ethAddress, timestamp, tokenAddresses.land)
) {
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.

maybe this can be only one if, right?

Collapse the if/return/if/return chains for estate and parcel
authorization back into single || expressions, with the owners.length
guard inlined as a short-circuit condition. Also simplify the
concurrency env var parsing to a single parseInt + NaN check.
Restructure all new tests to follow describe/it semantic naming:
- describe blocks use "when"/"and" for context
- it blocks use "should" for behavior
- Variables and setup moved to beforeEach, not inside it()
- afterEach added for mock cleanup
- Scoped mocks to the describe context where they are used
@LautaroPetaccio LautaroPetaccio merged commit 8f3a8e7 into main Apr 14, 2026
4 checks passed
@LautaroPetaccio LautaroPetaccio deleted the fix/crash-guards-null-safety branch April 14, 2026 18: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.

4 participants