yikart/AiToEarn · error · Error

上传失败: ${uploadResponse.statusText}

Error message

上传失败: ${uploadResponse.statusText}

What it means

thrown inside uploadToOss when the direct PUT of the file to the presigned OSS/S3 URL returns a non-ok response. It surfaces uploadResponse.statusText, which for CORS-direct uploads is often generic (e.g. 'Forbidden'), since the browser cannot read the error body cross-origin.

Source

Thrown at project/aitoearn-web/src/api/materials/material.api.ts:190

        xhr.open('PUT', uploadUrl)
        xhr.setRequestHeader('Content-Type', contentType)
        xhr.send(uploadFile)
      })
    }
    else {
      // 不使用进度回调的简单版本 (R2 使用 PUT 请求)

      const uploadResponse = await fetch(uploadUrl, {
        method: 'PUT',
        body: uploadFile,
        headers: {
          'Content-Type': contentType,
        },
        signal: opts.signal,
      })

      if (!uploadResponse.ok) {
        throw new Error(`上传失败: ${uploadResponse.statusText}`)
      }

      // 返回确认接口返回的最终访问URL
      return confirmUpload(presignedData.id, presignedData.url, opts.publicUploadId)
    }
  }
  catch (error) {
    console.error('上传文件失败:', error)
    throw error
  }
}

/**
 * 批量删除媒体资源
 * 根据ID列表批量删除媒体资源。
 */
export function batchDeleteMedia(ids: string[]) {
  return http.delete('media/ids', { ids })

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. Ensure the Content-Type sent in the PUT exactly equals the type used when requesting the presigned POST data.
  2. Shorten presign-to-upload time or increase the presigned expiration; re-request presigned data on retry.
  3. Verify the OSS/S3 bucket CORS configuration allows PUT from the web origin.
  4. Log uploadResponse.status alongside statusText — statusText is often empty or generic in browsers.
  5. Confirm optimizeImageForUpload hasn't changed the file type after presigning.

Example fix

// before
const fileName = getUploadFileName(uploadFile, opts.publicUploadId)
const presignedData = await getPresignedPostData(...) // signed with old contentType
// after (presign AFTER optimization, with the final type)
const uploadFile = await optimizeImageForUpload(file, { signal })
const contentType = uploadFile.type
const presignedData = await getPresignedPostData({ fileName, fileSize: uploadFile.size, contentType })
Defensive patterns

Strategy: retry

Validate before calling

// before PUT: confirm the type matches the signed type and URL is fresh
if (contentType !== presignedData.contentType) throw new Error('Content-Type 与签名不一致')
if (Date.now() > presignedData.expiresAt) throw new Error('签名已过期,请重新获取')

Type guard

function isUploadHttpError(e: unknown): e is Error {
  return e instanceof Error && e.message.startsWith('上传失败:')
}

Try / catch

try {
  const url = await uploadToOss(file, { signal })
}
catch (e) {
  if (e instanceof Error && e.message.startsWith('上传失败:')) {
    const url = await uploadToOss(file, { signal }) // fresh presign inside retry
  }
  else throw e
}

Prevention

When it happens

Trigger: PUTting the file to the presigned URL when: presigned data expired (signature/expiration mismatch), Content-Type header doesn't exactly match the one signed, key doesn't match, bucket CORS blocks the request, or storage returns 5xx.

Common situations: Slow networks where the presigned URL expires before upload finishes; contentType changed between presign and PUT (e.g. after optimizeImageForUpload re-encodes the file to a different type); bucket CORS misconfiguration; clock skew invalidating signatures.

Related errors


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