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
- Ensure the Content-Type sent in the PUT exactly equals the type used when requesting the presigned POST data.
- Shorten presign-to-upload time or increase the presigned expiration; re-request presigned data on retry.
- Verify the OSS/S3 bucket CORS configuration allows PUT from the web origin.
- Log uploadResponse.status alongside statusText — statusText is often empty or generic in browsers.
- 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
- Request presigned data AFTER image optimization so contentType matches the final file.
- Set the exact same Content-Type header as signed; never let fetch infer it.
- Configure bucket CORS for PUT from your web origin and verify with a browser test.
- Refresh presigned URLs on retry instead of reusing possibly expired ones.
- Watch for clock skew on clients in environments with expired-signature patterns.
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
- Relay uploadSign returned no uploadUrl: ${JSON.stringify(sig
- 文件上传失败
- 获取上传签名失败
- 上传确认失败
- VideoUploadVidNotFound
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/7817189e380ee935.
Report an issue: GitHub.