{"record":{"id":"09dc3d4f333e1afb","repo":"signalapp/Signal-Server","slug":"status-409","errorCode":null,"errorMessage":".status(409)","messagePattern":"\\.status\\(409\\)","errorType":"http","errorClass":"WebApplicationException","httpStatus":409,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java","lineNumber":671,"sourceCode":"    } catch (final CancellationException e) {\n      logger.error(\"cancelled while delivering multi-recipient messages\", e);\n      throw new InternalServerErrorException(\"delivery cancelled\");\n    } catch (final ExecutionException e) {\n      logger.error(\"partial failure while delivering multi-recipient messages\", e.getCause());\n      throw new InternalServerErrorException(\"failure during delivery\");\n    } catch (final MessageTooLargeException e) {\n      throw new WebApplicationException(Status.REQUEST_ENTITY_TOO_LARGE);\n    } catch (final MultiRecipientMismatchedDevicesException e) {\n      final List<AccountMismatchedDevices> accountMismatchedDevices =\n          e.getMismatchedDevicesByServiceIdentifier().entrySet().stream()\n              .filter(entry -> !entry.getValue().missingDeviceIds().isEmpty() || !entry.getValue().extraDeviceIds().isEmpty())\n              .map(entry -> new AccountMismatchedDevices(entry.getKey(),\n                  new MismatchedDevicesResponse(entry.getValue().missingDeviceIds(), entry.getValue().extraDeviceIds())))\n              .toList();\n\n      if (!accountMismatchedDevices.isEmpty()) {\n        throw new WebApplicationException(Response\n            .status(409)\n            .type(MediaType.APPLICATION_JSON_TYPE)\n            .entity(accountMismatchedDevices)\n            .build());\n      }\n\n      final List<AccountStaleDevices> accountStaleDevices =\n          e.getMismatchedDevicesByServiceIdentifier().entrySet().stream()\n              .filter(entry -> !entry.getValue().staleDeviceIds().isEmpty())\n              .map(entry -> new AccountStaleDevices(entry.getKey(),\n                  new StaleDevicesResponse(entry.getValue().staleDeviceIds())))\n              .toList();\n\n      throw new WebApplicationException(Response\n          .status(410)\n          .type(MediaType.APPLICATION_JSON)\n          .entity(accountStaleDevices)\n          .build());\n    } catch (final MessageDeliveryNotAllowedException e) {","sourceCodeStart":653,"sourceCodeEnd":689,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java#L653-L689","documentation":"sendMultiRecipientMessage returns HTTP 409 Conflict when one or more recipient accounts have mismatched devices: the client encrypted for a device set that differs from the server's current set of active devices. The 409 body is a JSON list of AccountMismatchedDevices, each carrying the account id plus missingDeviceIds and extraDeviceIds so the sender can re-derive session keys and resend.","triggerScenarios":"POSTing a multi-recipient message sealed for device IDs that are stale relative to server state — a recipient added a new device (missing) or removed/retired one (extra) after the sender last fetched keys.","commonSituations":"Recipients linking a new device or re-registering (which bumps device IDs) while senders cache old key material; bulk group sends where at least one member changed devices; long-lived clients not refreshing prekeys/device lists.","solutions":["Parse the 409 body's missing/extra device IDs, fetch new prekeys for missing devices, drop sessions for extra devices, and re-encrypt/resend","Refresh the device list and prekeys for affected recipients before the next send","Re-run your normal single-recipient 409 handling per mismatched account","If it happens constantly, check for registration races causing device churn"],"exampleFix":"// before: treat any non-2xx as failure and abort the whole group send\nif (!response.isSuccessful()) throw new IOException(\"send failed\");\n// after: reconcile on 409\nif (response.code() == 409) {\n  List<AccountMismatchedDevices> mm = parseMismatched(response.body());\n  for (AccountMismatchedDevices a : mm) {\n    refreshSessionsFor(a.accountId(), a.mismatchedDevices().missingDeviceIds(), a.mismatchedDevices().extraDeviceIds());\n  }\n  resend();\n}","handlingStrategy":"fallback","validationCode":"// refresh recipient device lists before a multi-recipient send\nfor (Recipient r : recipients) {\n  deviceListCache.refreshIfStale(r, maxAgeSeconds = 300);\n}","typeGuard":null,"tryCatchPattern":"if (response.code() == 409) {\n  List<AccountMismatchedDevices> mismatches = parseBody(response);\n  mismatches.forEach(m -> sessionStore.rebuildSessions(m.accountId(), m.missing(), m.extra()));\n  resend();\n}","preventionTips":["Keep recipient device-list caches short-lived","Handle 409 reconciliation as a first-class flow, not an error path","Fetch fresh prekeys for any missing device before re-encrypting","Drop sessions for extra device IDs promptly"],"tags":["http","encryption","device-management","conflict"],"backgroundTag":"http-error-response","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"}