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

  1. Verify the client actually completed the storage upload before calling confirmUploadByUser
  2. Re-run the upload with a fresh presigned URL if it expired
  3. Check storage configuration (bucket, endpoint, credentials, path prefix) matches between upload and confirm
  4. 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

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


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/0fa8581677b51474. Report an issue: GitHub.