{"record":{"id":"5791c6fbb4e4dc23","repo":"yikart/AiToEarn","slug":"responsecode-s3downloadfilefailed-5791c6","errorCode":"ResponseCode.S3DownloadFileFailed","errorMessage":"S3DownloadFileFailed","messagePattern":"S3DownloadFileFailed","errorType":"error_code","errorClass":"AppException","httpStatus":null,"severity":"error","filePath":"project/aitoearn-backend/libs/aws-s3/src/s3.service.ts","lineNumber":73,"sourceCode":"    const command = new HeadObjectCommand({\n      Bucket: this.config.bucketName,\n      Key: objectPath,\n    })\n    return await this.client.send(command)\n  }\n\n  async putObjectFromUrl(\n    url: string,\n    objectPath: string,\n  ) {\n    try {\n      await this.headObject(objectPath)\n      return { path: objectPath, exists: true }\n    }\n    catch {\n      const response = await fetch(url)\n      if (response.body === null) {\n        throw new AppException(ResponseCode.S3DownloadFileFailed)\n      }\n      const contentType = response.headers.get('content-type') || undefined\n      return this.putObject(objectPath, response.body, contentType)\n    }\n  }\n\n  // 生成预签名上传 URL\n  async getUploadSignPost(objectPath: string, contentType?: string) {\n    const result = await createPresignedPost(this.signingClient, {\n      Bucket: this.config.bucketName,\n      Key: objectPath,\n      Expires: this.config.signExpires,\n      Conditions: contentType ? [['eq', '$Content-Type', contentType]] : undefined,\n      Fields: contentType ? { 'Content-Type': contentType } : undefined,\n    })\n    return result\n  }\n","sourceCodeStart":55,"sourceCodeEnd":91,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/libs/aws-s3/src/s3.service.ts#L55-L91","documentation":"S3DownloadFileFailed is thrown by S3Service.putObjectFromUrl when it fetches the source URL and response.body is null, i.e. there is no readable body to stream into object storage. The service first checks if the object already exists; if not, it downloads from the URL and re-uploads it — a null body aborts that.","triggerScenarios":"The remote URL returns an empty/204 body, a redirect to an empty response, a network-layer failure surfacing as a body-less response, or the URL points at a resource that no longer exists (some servers return null body with error statuses that fetch doesn't throw on depending on setup).","commonSituations":"Importing an asset from a third-party URL that now 404s or redirects; expired signed URL for the source file; hotlink protection returning empty responses; timeout between fetch and body read.","solutions":["Verify the source URL is reachable and returns a 200 with a body (curl -I)","Re-generate or refresh signed/expired source URLs","Add retry with backoff around putObjectFromUrl for transient network failures","Check the response status code before calling the endpoint and fail fast with a clearer error"],"exampleFix":"// before\nawait s3Service.putObjectFromUrl(url, path) // url expired\n// after\nconst head = await fetch(url, { method: 'HEAD' })\nif (!head.ok) throw new Error(`source unavailable: ${head.status}`)\nawait s3Service.putObjectFromUrl(url, path)","handlingStrategy":"retry","validationCode":"const probe = await fetch(url, { method: 'HEAD' })\nif (!probe.ok) throw new Error(`source URL not downloadable: ${probe.status}`)","typeGuard":null,"tryCatchPattern":"try {\n  await s3Service.putObjectFromUrl(url, path)\n} catch (e) {\n  if (e instanceof AppException && e.code === ResponseCode.S3DownloadFileFailed) {\n    // retry with backoff or mark import as failed with source-URL context\n  } else throw e\n}","preventionTips":["Verify source URLs (200 + body) before import","Refresh signed URLs before use; track expiry","Wrap remote imports in retry with exponential backoff","Log the source URL with the failure for diagnosis"],"tags":["s3","network","download","fetch"],"backgroundTag":"download-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}