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}) 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
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"
Context
Neo.form.field.FileUpload.createUrlsubstitutes{param}tokens withString(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
#15642review as a pre-existing sharp edge (correctly out of scope there). The null-URL guards from#15637cover 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)
#15637's documented-optional-config contract remains intact.createUrlthrows before returning a URL./delete/1) keep working.0,false, and''retain normal JS string substitution.Contract Ledger
FileUpload#createUrl(urlPattern, params)#15637optional-pattern contract + this ticketdocumentStatusUrl/documentDeleteUrl/downloadUrlgettersbeforeGet*hookscreateUrlsemanticsdocumentIdcannot become a network URL; token-free constants remain validtypeof ... === 'function'branchAcceptance Criteria
createUrlthrows when a pattern token's supplied value is null/undefined; JSDoc records the throw condition and the nullish-pattern pass-through'/documents/null'expectation with a throw assertion, and separately proves token-free constants plus0/ empty-string values remain validOut of Scope
#15637)#15636)Related
#15637,#15636, PR#15642review 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"