{"record":{"id":"b774cb4fa5fed2a7","repo":"signalapp/Signal-Server","slug":"source-object-not-found","errorCode":null,"errorMessage":"Source object not found","messagePattern":"Source object not found","errorType":"http","errorClass":"ClientErrorException","httpStatus":410,"severity":"warning","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java","lineNumber":771,"sourceCode":"      @NotNull\n      @Valid final ArchiveController.CopyMediaRequest copyMediaRequest)\n      throws BackupFailedZkAuthenticationException, BackupWrongCredentialTypeException, BackupPermissionException, BackupInvalidArgumentException {\n    if (account.isPresent()) {\n      throw new BadRequestException(\"must not use authenticated connection for anonymous operations\");\n    }\n\n    final AuthenticatedBackupUser backupUser =\n        backupManager.authenticateBackupUser(presentation.presentation, signature.signature, userAgent);\n    final BackupManager.CopyQuota copyQuota =\n        backupManager.getCopyQuota(backupUser, List.of(copyMediaRequest.toCopyParameters()), maxAttachmentSize);\n    final CopyResult copyResult = backupManager.copyToBackup(copyQuota).next()\n            .blockOptional()\n            .orElseThrow(() -> new IllegalStateException(\"Non empty copy request must return result\"));\n    backupMetrics.updateCopyCounter(copyResult, UserAgentTagUtil.getPlatformTag(userAgent));\n    return switch (copyResult.outcome()) {\n      case SUCCESS -> new CopyMediaResponse(copyResult.cdn());\n      case SOURCE_WRONG_LENGTH -> throw new BadRequestException(\"Invalid length\");\n      case SOURCE_NOT_FOUND -> throw new ClientErrorException(\"Source object not found\", Response.Status.GONE);\n      case OUT_OF_QUOTA ->\n          throw new ClientErrorException(\"Media quota exhausted\", Response.Status.REQUEST_ENTITY_TOO_LARGE);\n    };\n  }\n\n  public record CopyMediaBatchRequest(\n      @Schema(description = \"A list of media objects to copy from the attachments CDN to the backup CDN\")\n      @NotNull\n      @Size(min = 1, max = 1000)\n      List<@Valid CopyMediaRequest> items) {}\n\n  public record CopyMediaBatchResponse(\n\n      @Schema(description = \"Detailed outcome information for each copy request in the batch\")\n      List<Entry> responses) {\n\n    public record Entry(\n        @Schema(description = \"\"\"","sourceCodeStart":753,"sourceCodeEnd":789,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java#L753-L789","documentation":"When the copy outcome is SOURCE_NOT_FOUND, the controller throws ClientErrorException(\"Source object not found\", HTTP 410 GONE): the media object the request asked to copy does not exist on the source CDN, so the backup copy cannot proceed.","triggerScenarios":"Calling copyMedia referencing a source CDN object (cdn number + key) that has been deleted, expired, or never uploaded.","commonSituations":"Source attachments pruned by temporary-message/attachment expiration before backup copy; stale message DB referencing deleted media; wrong CDN number or key after a CDN migration; new device restoring a DB without the media.","solutions":["Check the message for missing media and skip/backfill it instead of copying (HTTP 410 is permanent).","Re-download or request re-sending of the attachment from the sender, then copy the new object.","Verify the CDN number and key in CopyMediaObject match current CDN layout (migration may have renumbered).","If automatic backup copy keeps failing on stale references, prune dead attachment rows from the local database."],"exampleFix":"// before\ncopy(params); // throws 410 for expired media, aborts whole backup\n// after\ntry {\n  copy(params);\n} catch (ClientErrorException e) {\n  if (e.getResponse().getStatus() == 410) {\n    markAttachmentMissing(messageId); // skip and continue\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":"if (!sourceObjectExists(cdn, key)) { // HEAD before copy\n  markAttachmentMissing(messageId);\n  return;\n}","typeGuard":null,"tryCatchPattern":"try {\n  copyMedia(request);\n} catch (ClientErrorException e) {\n  if (e.getResponse().getStatus() == 410) {\n    skipMissingMedia(messageId); // 410 is permanent, do not retry\n  } else throw e;\n}","preventionTips":["Treat HTTP 410 as permanent: skip and backfill, never retry blindly.","Prune local DB references to expired/deleted attachments before bulk copy.","HEAD the source object (or batch-check existence) before issuing copy requests."],"tags":["http-410","cdn","resource-not-found","signal"],"backgroundTag":"resource-not-found","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}