Ordinary forms & companion mode
Use hidden JSON fields with ordinary HTML forms, validate file IDs on the server, and handle mixed multipart + ID contracts.
Ordinary HTML form (picker mode)
In picker mode every file — local or phone — goes through CrossUpload, and your form receives one consistent, ID-based contract.
<form method="post" action="/claims">
<label>Claim number <input name="claim" required></label>
<button type="button" id="photos">Upload photos</button>
<input type="hidden" id="photo_ids" name="photo_ids" value="[]">
<button type="submit">Submit claim</button>
</form>
<script src="https://app.crossupload.com/v1/widget.js"></script>
<script>
CrossUpload.attach({ target: '#photos', sessionEndpoint: '/api/crossupload/session', resultInput: '#photo_ids', maxFiles: 5 });
</script>
Server-side validation (required)
Hidden fields are attacker-controlled. Never trust IDs or URLs posted from them.
for posted in serde_json::from_str::<Vec<Posted>>(&form.photo_ids)? {
let f: Value = crossupload.get(format!("/v1/files/{}", posted.id)).await?; // secret key
let ok = f["status"] == "ready"
&& f["projectId"] == MY_PROJECT_ID
&& sessions_issued_to(user).contains(f["sessionId"].as_str().unwrap_or(""))
&& f["externalReference"] == format!("user:{}:claim:{}", user.id, claim.id);
if !ok { return Err(BadRequest("invalid attachment")); }
db.attach_file(claim.id, posted.id).await?; // store the ID, not the URL
}
A 404 means the ID doesn’t exist or belongs to another project — treat both as invalid.
Companion mode: the mixed contract
Companion mode adds “Upload from your phone” next to an existing <input type="file"> and leaves that input untouched. Your backend can therefore receive two kinds of files in one submission:
- local files as normal
multipart/form-dataparts (exactly as before), and - phone files as CrossUpload file IDs in a hidden field.
<form method="post" action="/applications" enctype="multipart/form-data">
<input type="file" id="cv" name="cv">
<input type="hidden" id="cv_phone" name="cv_phone_ids" value="[]">
<button type="submit">Apply</button>
</form>
<script>
CrossUpload.attach({ target: '#cv', mode: 'companion', sessionEndpoint: '/api/crossupload/session', resultInput: '#cv_phone' });
</script>
Handle both explicitly:
while let Some(field) = multipart.next_field().await? {
match field.name() {
Some("cv") if field.file_name().is_some_and(|n| !n.is_empty()) => {
let bytes = field.bytes().await?; // legacy local path
store_local_upload(&bytes).await?;
}
Some("cv_phone_ids") => {
let ids: Vec<Posted> = serde_json::from_str(&field.text().await?)?;
for p in ids { validate_and_attach(p.id).await?; } // CrossUpload path
}
_ => {}
}
}
The repository’s examples/customer-backend implements exactly this.
FileList from a URL, and assigning a URL to <input type="file"> does nothing. A multipart-only backend needs an adapter or a small change to accept file IDs. A fetch-Blob-File compatibility adapter is planned for a later release.Something unclear or wrong? Email master@crossupload.com.