yikart/AiToEarn · error · AppException
ResponseCode.AssetUploadFailed
ResponseCode.AssetUploadFailed
Error message
AssetUploadFailed
What it means
AssetUploadFailed is thrown by confirmUploadByUser when storage.headObject(asset.path) returns null, meaning the file the client was supposed to upload to object storage is not present at the expected key. The asset record exists (status Pending) but the actual bytes never arrived, so confirmation cannot proceed.
Source
Thrown at project/aitoearn-backend/libs/assets/src/assets.service.ts:362
async cleanupExpiredPendingAssets(olderThanSeconds: number): Promise<{ affectedCount: number }> {
return await this.assetRepository.markExpiredPendingAsFailed(olderThanSeconds)
}
async confirmUploadByUser(userId: string, assetId: string, userType: UserType = UserType.User): Promise<Asset> {
const asset = await this.assetRepository.getByIdAndUserId(assetId, userId, userType)
if (!asset) {
throw new AppException(ResponseCode.AssetNotFound)
}
if (asset.status !== AssetStatus.Pending) {
return asset
}
const headResult = await this.storage.headObject(asset.path)
if (!headResult) {
throw new AppException(ResponseCode.AssetUploadFailed)
}
if (this.options.maxSize != null && headResult.contentLength && headResult.contentLength >= this.options.maxSize) {
throw new AppException(ResponseCode.AssetTooLarge)
}
const expectedMimeType = asset.filename
? (mime.lookup(asset.filename) || asset.mimeType)
: asset.mimeType
const currentContentType = headResult.contentType
if (expectedMimeType && currentContentType !== expectedMimeType) {
const contentDisposition = expectedMimeType.startsWith('video/') ? 'inline' : undefined
await this.storage.copyObject(asset.path, {
contentType: expectedMimeType,
contentDisposition,
metadata: {
assetId: asset.id,View on GitHub (pinned to d3aa8bea5b)
Solutions
- Verify the client actually completed the storage upload before calling confirmUploadByUser
- Re-run the upload with a fresh presigned URL if it expired
- Check storage configuration (bucket, endpoint, credentials, path prefix) matches between upload and confirm
- Retry the whole initiate→upload→confirm flow
Example fix
// before
await client.call('confirmUpload', assetId) // upload actually failed silently
// after
const ok = await uploadToStorage(presignedUrl, file)
if (!ok) throw new Error('upload failed, do not confirm')
await client.call('confirmUpload', assetId) Defensive patterns
Strategy: retry
Validate before calling
const head = await fetch(uploadUrl, { method: 'HEAD' }).catch(() => null)
if (!head || !head.ok) throw new Error('object not present at storage path; re-upload first') Try / catch
try {
await assetsService.confirmUploadByUser(userId, assetId)
} catch (e) {
if (e instanceof AppException && e.code === ResponseCode.AssetUploadFailed) {
await reUploadAndConfirm(assetId, file) // retry whole upload flow
} else throw e
} Prevention
- Confirm client upload success (ETag/200) before calling confirm
- Use fresh presigned URLs; do not cache across sessions
- Keep bucket/prefix config consistent between services
- Monitor storage-layer upload errors to catch silent client failures
When it happens
Trigger: Client marks upload done without actually PUTting the file; upload to S3/OSS failed or was aborted midway; the object was uploaded to a different path/key than asset.path; the object was deleted between upload and confirm; wrong storage bucket configured.
Common situations: Client SDK swallows upload errors and still calls confirm; presigned URL expired so the PUT never happened; bucket/prefix misconfiguration after an env change; multipart upload aborted due to network loss.
Related errors
- VideoUploadVidNotFound
- Relay uploadSign returned no uploadUrl: ${JSON.stringify(sig
- S3DownloadFileFailed
- ChannelPlatformResponseInvalid
- ChannelPlatformMediaProcessingFailed
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/0fa8581677b51474.
Report an issue: GitHub.