Server Unavailable for Identity/Get operation

Issue Description

I was previously having an issue with v0.16.17 where my JMAP identities didn’t match aliases assigned to my user account.

I upgraded to v0.16.18 yesterday, especially after seeing that the changelog mentioned keeping identities in sync. However, now the JMAP API is giving a “serverUnavailable” response when clients attempt to retrieve identities via “Identity/get”.

Relevant Log Output

Request:

{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail","urn:ietf:params:jmap:submission"],"methodCalls":[["Identity/get",{"accountId":"b"},"0"]]}

Response:

{"methodResponses":[["error",{"type":"serverUnavailable","description":"This server is temporarily unavailable. Attempting this same operation later may succeed."},"0"]],"sessionState":"bf89f5d0"}

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.

on

The serverUnavailable response your client is seeing is a generic wrapper: in the JMAP method-error handling code, any internal/database error that isn’t a client-facing JMAP fault (invalidArguments, notFound, etc.) gets serialized back to the client as serverUnavailable, regardless of what actually failed on the backend. So the real cause is an internal error being raised inside the Identity/get handler, not a genuine “server busy, try later” situation.

In v0.16.18, Identity/get was changed to keep identities automatically in sync with the account’s e-mail addresses/aliases each time it’s called (creating missing identities and removing ones no longer valid, updating a stored hash on the account’s Principal record). That logic does a store commit as part of every Identity/get call, and if that commit hits an unexpected error (not a normal write-conflict retry, which is already handled), it will bubble up and get flattened into serverUnavailable in the JMAP response.

The actual underlying error will be recorded in the server’s tracing/event log at the time of the failed request. Could you check the server logs (webadmin Logs view or your Docker logs) for the corresponding event around the failed Identity/get call, and share the specific error type/message shown there? That will identify what’s actually failing (e.g. a principal lookup issue, a malformed alias, etc.) rather than the generic client-facing message.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Sharing trace logs from the event:

2026-08-21T14:02:54Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, remoteIp = 172.x.x.x, url = "/jmap/"
2026-08-21T14:02:54Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-08-21T14:02:54Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, contents = (null), code = 204, size = 0
2026-08-21T14:02:54Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, remoteIp = 172.x.x.x, url = "/jmap/"
2026-08-21T14:02:54Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-08-21T14:02:54Z DEBUG Cache hit (store.cache-hit) key = 1, collection = "accessToken"
2026-08-21T14:02:54Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, details = [["host", "mail.xxxxxx.com"], ["user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0"], ["content-length", "251"], ["accept", "*/*"], ["accept-encoding", "gzip, deflate, br, zstd"], ["accept-language", "en-US,en;q=0.9"], ["authorization", "Bearer xxxxxxxxxxxxxxxxxxxxxx"], ["content-type", "application/json"], ["dnt", "1"], ["origin", "https://webmail.xxxxxx.com"], ["priority", "u=0"], ["referer", "https://webmail.xxxxxx.com/"], ["sec-fetch-dest", "empty"], ["sec-fetch-mode", "cors"], ["sec-fetch-site", "same-site"], ["sec-gpc", "1"], ["te", "trailers"], ["via", "2.0 Caddy"], ["x-forwarded-for", "172.x.x.x"], ["x-forwarded-host", "mail.xxxxxx"], ["x-forwarded-proto", "https"]], contents = "{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail","urn:ietf:params:jmap:submission"],"methodCalls":[["Identity/set",{"accountId":"b","create":{"new-identity":{"name":"Matt","email":"[email protected]","textSignature":""}}},"0"]]}", size = 251
2026-08-21T14:02:54Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-08-21T14:02:54Z DEBUG Cache hit (store.cache-hit) key = 1, collection = "account"
2026-08-21T14:02:54Z DEBUG Cache hit (store.cache-hit) key = 1, collection = "domainId"
2026-08-21T14:02:54Z DEBUG Cache hit (store.cache-hit) key = 3, collection = "domainId"
2026-08-21T14:02:54Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 3
2026-08-21T14:02:54Z TRACE Write batch operation (store.data-write) elapsed = 0ms, total = 6
2026-08-21T14:02:54Z DEBUG JMAP method call (jmap.method-call) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, id = "Identity/set", accountId = 1, elapsed = 0ms
2026-08-21T14:02:54Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, contents = "{"methodResponses":[["Identity/set",{"accountId":"b","newState":"sq9aak","created":{"new-identity":{"id":"n"}}},"0"]],"sessionState":"bf89f5d0"}", code = 200, size = 144
2026-08-21T14:02:54Z DEBUG HTTP request URL (http.request-url) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, remoteIp = 172.x.x.x, url = "/jmap/"
2026-08-21T14:02:54Z TRACE Expression evaluation result (eval.result) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, id = "Http with id singleton", key = "allowedEndpoints", result = "Integer(200)"
2026-08-21T14:02:54Z DEBUG Cache hit (store.cache-hit) key = 1, collection = "accessToken"
2026-08-21T14:02:54Z TRACE HTTP request body (http.request-body) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, details = [["host", "mail.xxxxxx.com"], ["user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0"], ["content-length", "156"], ["accept", "*/*"], ["accept-encoding", "gzip, deflate, br, zstd"], ["accept-language", "en-US,en;q=0.9"], ["authorization", "Bearer xxxxxxxxxxxxxxxxxxxxxx"], ["content-type", "application/json"], ["dnt", "1"], ["origin", "https://webmail.xxxxxx.com"], ["priority", "u=4"], ["referer", "https://webmail.xxxxxx.com/"], ["sec-fetch-dest", "empty"], ["sec-fetch-mode", "cors"], ["sec-fetch-site", "same-site"], ["sec-gpc", "1"], ["te", "trailers"], ["via", "2.0 Caddy"], ["x-forwarded-for", "172.x.x.x"], ["x-forwarded-host", "mail.xxxxxx.com"], ["x-forwarded-proto", "https"]], contents = "{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail","urn:ietf:params:jmap:submission"],"methodCalls":[["Identity/get",{"accountId":"b"},"0"]]}", size = 156
2026-08-21T14:02:54Z DEBUG Cache hit (store.cache-hit) key = 1, collection = "account"
2026-08-21T14:02:54Z DEBUG Cache hit (store.cache-hit) key = 1, collection = "domainId"
2026-08-21T14:02:54Z DEBUG Cache hit (store.cache-hit) key = 3, collection = "domainId"
2026-08-21T14:02:54Z ERROR Data corruption detected (store.data-corruption) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, causedBy = "crates/store/src/write/serialize.rs:545", causedBy = "crates/store/src/dispatch/store.rs:46", causedBy = "crates/jmap/src/identity/get.rs:178", accountId = 1, causedBy = "Identity/get"
2026-08-21T14:02:54Z TRACE HTTP response body (http.response-body) listenerId = "https", localPort = 443, remoteIp = 10.x.x.x, remotePort = 45914, contents = "{"methodResponses":[["error",{"type":"serverUnavailable","description":"This server is temporarily unavailable. Attempting this same operation later may succeed."},"0"]],"sessionState":"bf89f5d0"}", code = 200, size = 196

As a quick update, I reverted back to v0.16.17 and everything is working as expected. So I am wondering if the reported data corruption is accurate? Or just an issue with changes made in v0.16.18?

Fixed in v0.16.19 (yet to be released).