{"record":{"id":"d964c82b1b792914","repo":"yikart/AiToEarn","slug":"error-d964c8","errorCode":null,"errorMessage":"上传已取消","messagePattern":"上传已取消","errorType":"exception","errorClass":"DOMException","httpStatus":null,"severity":"warning","filePath":"project/aitoearn-web/src/api/materials/material.api.ts","lineNumber":111,"sourceCode":"  if (!confirmResponse || confirmResponse.code !== 0)\n    throw new Error(confirmResponse?.message || '上传确认失败')\n\n  return confirmResponse.data?.url || fallbackUrl\n}\n\n/**\n * 上传文件到OSS (前端直传 AWS S3)\n */\nexport async function uploadToOss(\n  file: File | Blob,\n  options?: UploadToOssOptions | ((prog: number) => void),\n): Promise<string> {\n  try {\n    const opts: UploadToOssOptions\n      = typeof options === 'function' ? { onProgress: options } : (options ?? {})\n\n    if (opts.signal?.aborted) {\n      throw new DOMException('上传已取消', 'AbortError')\n    }\n\n    const uploadFile = await optimizeImageForUpload(file, { signal: opts.signal })\n\n    const fileName = getUploadFileName(uploadFile, opts.publicUploadId)\n\n    const fileSize = uploadFile.size\n    const contentType = uploadFile.type || 'application/octet-stream'\n\n    // 获取 presigned post 数据\n    const presignedData = await getPresignedPostData(fileName, fileSize, contentType, opts.publicUploadId)\n\n    // R2 使用 PUT 请求直接上传到 uploadUrl，不需要 FormData\n    const uploadUrl = presignedData.uploadUrl\n\n    // 直传文件到 AWS S3 (支持进度回调)\n    if (opts.onProgress) {\n      return new Promise<string>((resolve, reject) => {","sourceCodeStart":93,"sourceCodeEnd":129,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-web/src/api/materials/material.api.ts#L93-L129","documentation":" DOMException with name 'AbortError' thrown at the start of uploadToOss when opts.signal.aborted is already true — i.e. the caller's AbortController was aborted before the upload began. It is the cancel-signal fast path so an aborted upload never starts network work.","triggerScenarios":"The user/UI cancels an upload (abort() on the AbortController) before or as uploadToOss is invoked; a component unmount aborts the controller while the promise is being created; the same signal reused after a previous upload was cancelled.","commonSituations":"Users closing a modal/leaving a page mid-upload; React strict-mode double effects aborting the first attempt; reusing one AbortController across sequential uploads without resetting it.","solutions":["Treat DOMException with name === 'AbortError' as an expected cancellation, not a failure — skip error toasts/logging.","Create a fresh AbortController per upload attempt.","If cancellation was unintentional (e.g. premature unmount), fix the effect cleanup so the controller isn't aborted too early.","Check that the cancel UI only aborts the upload it owns."],"exampleFix":"// before\ntry { await uploadToOss(file, { signal }) } catch (e) { showError(e) }\n// after\ntry {\n  await uploadToOss(file, { signal })\n}\ncatch (e) {\n  if (e instanceof DOMException && e.name === 'AbortError') return // cancelled\n  showError(e)\n}","handlingStrategy":"try-catch","validationCode":"// before starting an upload\nif (controller.signal.aborted) return // skip entirely; don't even call uploadToOss","typeGuard":"function isAbortError(e: unknown): e is DOMException {\n  return e instanceof DOMException && e.name === 'AbortError'\n}","tryCatchPattern":"try {\n  await uploadToOss(file, { signal: controller.signal })\n}\ncatch (e) {\n  if (isAbortError(e)) return // expected cancellation\n  throw e\n}","preventionTips":["Create a new AbortController per upload attempt; never reuse aborted ones.","Filter AbortError from global error reporting/toasts — it is user-initiated.","In React effects, only abort uploads owned by that effect instance.","Debounce rapid unmount/remount cycles (strict mode) to avoid premature aborts."],"tags":["upload","abort","cancellation","domexception"],"backgroundTag":"abort-error","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}