Context
Neo.form.field.FileUpload documents documentStatusUrl as optional ("The status check phase is optional. If no URL specified, the file is taken to be downloadable."). In practice, omitting it (or downloadUrl) crashes the upload flow after a successful upload:
TypeError: Cannot read properties of null (reading 'replace')
at FileUpload.createUrl (src/form/field/FileUpload.mjs:792)
at FileUpload.beforeGetDocumentStatusUrl (src/form/field/FileUpload.mjs:804)
at FileUpload.onUploadDone (src/form/field/FileUpload.mjs:557)Live receipt: an app configured with only uploadUrl + documentDeleteUrl uploads fine (201), then onUploadDone reads me.documentStatusUrl, whose beforeGet hook calls createUrl(null, …) — urlPattern.replace on null.
Live latest-open sweep: checked latest 20 open issues at 2026-07-21 ~15:35Z; no equivalent found. A2A in-flight claim sweep (last 60 min): no competing claim.
The Problem
Three beforeGet*Url hooks call createUrl(urlPattern, params) unconditionally:
beforeGetDocumentStatusUrl (src/form/field/FileUpload.mjs:801-807)
beforeGetDocumentDeleteUrl (src/form/field/FileUpload.mjs:809-815)
beforeGetDownloadUrl (src/form/field/FileUpload.mjs:817-823)
createUrl (src/form/field/FileUpload.mjs:790-795) runs urlPattern.replace(...) with no null guard. The delete URL is read only when a document exists, so it is masked in practice; status and download are read on the normal success path, so a minimally-configured field crashes after the server already stored the file — the worst failure shape: durable write, broken client state, no error surfaced to the user.
The same class bites beforeGetMaxSize callers less (it null-guards explicitly at :825-828), which makes the missing guards in the URL hooks read as an oversight rather than a contract.
The Fix
Null-safe hooks, preserving the documented optionality:
beforeGetDocumentStatusUrl(documentStatusUrl) {
const me = this;
return typeof documentStatusUrl === 'function'
? documentStatusUrl.call(me, me)
: documentStatusUrl && me.createUrl(documentStatusUrl, {[me.documentIdParameter]: me.documentId});
}and the identical guard for beforeGetDocumentDeleteUrl and beforeGetDownloadUrl (return the value unchanged when null — callers already branch on falsy).
Acceptance Criteria
Out of Scope
- The upload-transport seam — that is
#15636
- Any new server-side contract
Related
#15636 (transport seam — sibling, larger arc)
src/form/field/FileUpload.mjs:790-823 (hook sites)
Origin Session ID: d8a51237-4fcc-4171-8071-a391da0be361
Retrieval Hint: "FileUpload beforeGetDocumentStatusUrl null createUrl replace crash"
Context
Neo.form.field.FileUploaddocumentsdocumentStatusUrlas optional ("The status check phase is optional. If no URL specified, the file is taken to be downloadable."). In practice, omitting it (ordownloadUrl) crashes the upload flow after a successful upload:TypeError: Cannot read properties of null (reading 'replace') at FileUpload.createUrl (src/form/field/FileUpload.mjs:792) at FileUpload.beforeGetDocumentStatusUrl (src/form/field/FileUpload.mjs:804) at FileUpload.onUploadDone (src/form/field/FileUpload.mjs:557)Live receipt: an app configured with only
uploadUrl+documentDeleteUrluploads fine (201), thenonUploadDonereadsme.documentStatusUrl, whosebeforeGethook callscreateUrl(null, …)—urlPattern.replaceonnull.Live latest-open sweep: checked latest 20 open issues at 2026-07-21 ~15:35Z; no equivalent found. A2A in-flight claim sweep (last 60 min): no competing claim.
The Problem
Three
beforeGet*Urlhooks callcreateUrl(urlPattern, params)unconditionally:beforeGetDocumentStatusUrl(src/form/field/FileUpload.mjs:801-807)beforeGetDocumentDeleteUrl(src/form/field/FileUpload.mjs:809-815)beforeGetDownloadUrl(src/form/field/FileUpload.mjs:817-823)createUrl(src/form/field/FileUpload.mjs:790-795) runsurlPattern.replace(...)with no null guard. The delete URL is read only when a document exists, so it is masked in practice; status and download are read on the normal success path, so a minimally-configured field crashes after the server already stored the file — the worst failure shape: durable write, broken client state, no error surfaced to the user.The same class bites
beforeGetMaxSizecallers less (it null-guards explicitly at:825-828), which makes the missing guards in the URL hooks read as an oversight rather than a contract.The Fix
Null-safe hooks, preserving the documented optionality:
beforeGetDocumentStatusUrl(documentStatusUrl) { const me = this; return typeof documentStatusUrl === 'function' ? documentStatusUrl.call(me, me) : documentStatusUrl && me.createUrl(documentStatusUrl, {[me.documentIdParameter]: me.documentId}); }and the identical guard for
beforeGetDocumentDeleteUrlandbeforeGetDownloadUrl(return the value unchanged when null — callers already branch on falsy).Acceptance Criteria
uploadUrlcompletes an upload reachingdownloadablewithout throwing (unit spec: mocked XHR success, nodocumentStatusUrl/downloadUrlset){documentId}token, function form)examples/form/field/fileupload/is unchanged and greenOut of Scope
#15636Related
#15636(transport seam — sibling, larger arc)src/form/field/FileUpload.mjs:790-823(hook sites)Origin Session ID: d8a51237-4fcc-4171-8071-a391da0be361
Retrieval Hint: "FileUpload beforeGetDocumentStatusUrl null createUrl replace crash"