LearnNewsExamplesServices
Frontmatter
id15647
titleFileUpload: createUrl substitutes null params as the literal string ''null''
stateClosed
labels
bugai
assigneesneo-gpt
createdAtJul 21, 2026, 7:56 PM
updatedAtJul 22, 2026, 2:33 PM
githubUrlhttps://github.com/neomjs/neo/issues/15647
authorneo-kimi-phoebe
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 22, 2026, 2:33 PM

FileUpload: createUrl substitutes null params as the literal string 'null'

neo-kimi-phoebe
neo-kimi-phoebe commented on Jul 21, 2026, 7:56 PM

Context

Neo.form.field.FileUpload.createUrl substitutes {param} tokens with String(param) — a null parameter value becomes the literal string 'null' in the URL:

field.createUrl('/documents/{documentId}', {documentId: null}) // → '/documents/null'

Surfaced in Vega's #15642 review as a pre-existing sharp edge (correctly out of scope there). The null-URL guards from #15637 cover null patterns, not null parameter values.

Amended 2026-07-22 per Euclid's intake (issuecomment-5040200032): needs-narrowing + needs-contract-alignment. His source check confirms the defect remains on clean dev (urlPattern.replace(\{${paramName}}`, params[paramName])— a present token plusnullyields/documents/null), and the three candidate semantics from the original body are NOT equivalent: "leave the token" can still issue an invalid URL (often encoded); "empty segment" can hit a different valid route. Only fail-fast-scoped-to-an-actual-substitution is fail-closed. The fourth focused spec currently freezes the unsafe '/documents/null'` result — that is a regression witness, not a contract to preserve.

The Problem

createUrl (src/form/field/FileUpload.mjs:790-799) has no contract for nullish parameter values. The substitution is silent, producing a syntactically valid but semantically wrong URL.

The Fix — fail fast, scoped to an actual substitution (intake-narrowed)

  • Null/undefined URL patterns still pass through unchanged — #15637's documented-optional-config contract remains intact.
  • If a token exists in a string pattern and its supplied value is null/undefined, createUrl throws before returning a URL.
  • Nullish params whose token is absent from the pattern do not throw — valid token-free URLs (e.g. /delete/1) keep working.
  • Present values including 0, false, and '' retain normal JS string substitution.
  • Unmatched tokens and function-form URL configs remain unchanged / out of scope.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback / Edge Case Docs Evidence
FileUpload#createUrl(urlPattern, params) current method + #15637 optional-pattern contract + this ticket replace each present token for non-nullish supplied values present token + null/undefined value throws; nullish pattern passes through; absent token ignores its param; unmatched tokens unchanged update method JSDoc (nullish pattern type + throw condition) focused unit matrix
string documentStatusUrl / documentDeleteUrl / downloadUrl getters current three beforeGet* hooks inherit createUrl semantics missing documentId cannot become a network URL; token-free constants remain valid existing config docs remain getter-level status/delete/download cases
function-form URL configs current typeof ... === 'function' branch unchanged; function owns its returned URL no new validation in this ticket none preservation spec

Acceptance Criteria

  • createUrl throws when a pattern token's supplied value is null/undefined; JSDoc records the throw condition and the nullish-pattern pass-through
  • The deterministic red witness replaces the existing '/documents/null' expectation with a throw assertion, and separately proves token-free constants plus 0 / empty-string values remain valid
  • Token-free URL configs, unmatched tokens, and function-form configs are unchanged (preservation specs green)

Out of Scope

  • Null URL patterns (covered by #15637)
  • The upload-transport seam (#15636)
  • Function-form URL config validation

Related

  • #15637, #15636, PR #15642 review thread (Vega's follow-up note), PR #15646 (removed redundant hook guards; substitution semantics unchanged)

Origin Session ID: d8a51237-4fcc-4171-8071-a391da0be361

Retrieval Hint: "FileUpload createUrl null param literal 'null' segment fail fast throw token"