{"record":{"id":"503ba0c386245d1c","repo":"signalapp/Signal-Server","slug":"media-quota-exhausted","errorCode":null,"errorMessage":"Media quota exhausted","messagePattern":"Media quota exhausted","errorType":"http","errorClass":"ClientErrorException","httpStatus":413,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java","lineNumber":773,"sourceCode":"      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 = \"\"\"\n            The outcome of the copy attempt.\n            A 200 indicates the object was successfully copied.","sourceCodeStart":755,"sourceCodeEnd":791,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/ArchiveController.java#L755-L791","documentation":"ArchiveController.copyMedia throws this (HTTP 413) when BackupManager reports OUT_OF_QUOTA: copying a media object from the attachments CDN to the backup CDN would exceed the account's backup media storage quota. The server refuses the copy before consuming storage.","triggerScenarios":"POST to the anonymous copy-media batch endpoint where one or more items' media would push total backed-up media bytes over the quota computed by backupManager.getCopyQuota.","commonSituations":"Backups accumulating past the paid/allowed quota; retrying copy batches after earlier partial failures; large attachment uploads when the user has almost no quota left; stale client assumptions about available space.","solutions":["Delete unneeded backed-up media via the delete-media endpoint to free quota, then retry the copy","Check remaining quota before issuing copy batches and chunk requests to fit within it","Surface the 413 to the user so they can upgrade their backup plan or clear old media","Do not blindly retry: the error is deterministic until storage is freed"],"exampleFix":"// before\ncopyAllMedia(items); // throws 413 when quota is exhausted\n// after\nlong remaining = fetchRemainingQuota();\nList<CopyMediaRequest> fitting = items.stream()\n    .filter(i -> i.uploadLength() <= remaining)\n    .toList();\nif (fitting.size() < items.size()) promptUserToFreeQuota();\ncopyAllMedia(fitting);","handlingStrategy":"validation","validationCode":"long totalAfterCopy = currentBackupBytes + items.stream().mapToLong(CopyMediaRequest::uploadLength).sum();\nif (totalAfterCopy > quotaLimit) { freeSpaceOrNotifyUser(); return; }","typeGuard":null,"tryCatchPattern":"try { copyMedia(items); } catch (ClientZonedDateTimeException e) {} catch (WebApplicationException e) { if (e.getResponse().getStatus() == 413) { handleQuotaExhausted(); } }","preventionTips":["Track quota usage locally after every copy/delete","Chunk copy batches to stay under quota","Treat 413 as terminal, not retryable"],"tags":["http-413","quota","backups","storage"],"backgroundTag":"payload-too-large","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}