You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Android Gmail (and possibly other clients) abort ActiveSync Sync requests after ~30 seconds of no response body bytes, then enter a confused state that does not self-heal. Horde currently buffers the entire Sync WBXML body before sending it to the client. The existing maxresponsetime time budget (introduced in 3.0.0-RC4) mitigates large batches but does not fix connection silence, especially for bidirectional Drafts sync and heavy initial mailbox sync.
This issue tracks incremental Sync response delivery so the connection stays active while messages are assembled.
Observed: FullDraftsUpSyncSocketTimeout at 30s while the server was still working; repeated on retry; client SSL errors afterward.
Server logs show Sync time budget (25s) reached … MOREAVAILABLEafter the client already timed out.
Recovery today requires delete-account + server state reset — we should remove the primary server-side trigger.
Root cause
Horde_Rpc_ActiveSync captures the full response in ob_start(1MB) and sends it only in sendOutput() with Content-Length (horde/rpc ActiveSync.php) — no body bytes reach the client until the handler finishes.
Horde_ActiveSync_Request_Sync may buffer the entire Commands section in a temp stream when time budget is enabled (for correct MoreAvailable ordering before Commands).
Incoming client Commands (e.g. draft upload via FullDraftsUpSync) and IMAP change polling run before or outside the time budget’s effective protection.
Gmail’s timeout is on read blocking until HTTP response data arrives, not on total sync duration while bytes flow.
Proposed solution
Phase 1 — RPC streaming (critical)
Sync-specific path in Horde_Rpc_ActiveSync: skip full-response output buffering for Cmd=Sync; flush WBXML incrementally (likely chunked transfer-encoding for Sync only).
Keep Content-Length / buffered path for GetAttachment, ItemOperations, etc. (Bug #12486).
Phase 2 — Sync handler incremental export
Encoder::flushOutput() after each successful sendNextChange().
Count-based maxmessagesperresponse (e.g. default 10): if more changes than cap, emit MoreAvailablebeforeCommands, then stream up to cap.
Disable command temp-buffer when streaming is enabled (_useSyncCommandsBuffer).
Phase 3 — Guards
maxmessagetime — per-message assembly cap (pathological single draft/MIME).
maxrequestduration — whole-request wall clock including incoming import phase.
maxresponsetime: keep as optional legacy fallback (0 default once streaming is validated); not the primary keep-alive mechanism after streaming.
Config (proposed)
Key
Purpose
activesync.sync.streaming
Master switch (rollback to legacy)
activesync.sync.maxmessagesperresponse
Count-based batch cap
activesync.sync.maxmessagetime
Per-message assembly cap
activesync.sync.maxrequestduration
Whole-request wall clock
activesync.sync.maxresponsetime
Legacy export-phase time budget
Non-goals
Fixing Gmail’s slow follow-up after MOREAVAILABLE (client pacing).
Fixing client state recovery after a broken relationship (operational workaround remains).
Full H6 request/response pipeline rewrite (doc/todo.md) — align with that roadmap but ship a focused fix first.
Test plan
PHPUnit: MoreAvailable byte order before Commands; flush per message; pending changes preserved.
Integration: mock driver with delayed getMessage; assert TTFB < 5s with streaming.
Summary
Android Gmail (and possibly other clients) abort ActiveSync
Syncrequests after ~30 seconds of no response body bytes, then enter a confused state that does not self-heal. Horde currently buffers the entire Sync WBXML body before sending it to the client. The existingmaxresponsetimetime budget (introduced in 3.0.0-RC4) mitigates large batches but does not fix connection silence, especially for bidirectional Drafts sync and heavy initial mailbox sync.This issue tracks incremental Sync response delivery so the connection stays active while messages are assembled.
Motivation
Related to #77.
FullDraftsUpSyncSocketTimeoutat 30s while the server was still working; repeated on retry; client SSL errors afterward.Sync time budget (25s) reached … MOREAVAILABLEafter the client already timed out.Root cause
Horde_Rpc_ActiveSynccaptures the full response inob_start(1MB)and sends it only insendOutput()withContent-Length(horde/rpcActiveSync.php) — no body bytes reach the client until the handler finishes.Horde_ActiveSync_Request_Syncmay buffer the entireCommandssection in a temp stream when time budget is enabled (for correctMoreAvailableordering beforeCommands).Commands(e.g. draft upload viaFullDraftsUpSync) and IMAP change polling run before or outside the time budget’s effective protection.Proposed solution
Phase 1 — RPC streaming (critical)
Horde_Rpc_ActiveSync: skip full-response output buffering forCmd=Sync; flush WBXML incrementally (likely chunked transfer-encoding for Sync only).Phase 2 — Sync handler incremental export
Encoder::flushOutput()after each successfulsendNextChange().maxmessagesperresponse(e.g. default 10): if more changes than cap, emitMoreAvailablebeforeCommands, then stream up to cap._useSyncCommandsBuffer).Phase 3 — Guards
maxmessagetime— per-message assembly cap (pathological single draft/MIME).maxrequestduration— whole-request wall clock including incoming import phase.maxresponsetime: keep as optional legacy fallback (0default once streaming is validated); not the primary keep-alive mechanism after streaming.Config (proposed)
activesync.sync.streamingactivesync.sync.maxmessagesperresponseactivesync.sync.maxmessagetimeactivesync.sync.maxrequestdurationactivesync.sync.maxresponsetimeNon-goals
MOREAVAILABLE(client pacing).doc/todo.md) — align with that roadmap but ship a focused fix first.Test plan
MoreAvailablebyte order beforeCommands; flush per message; pending changes preserved.getMessage; assert TTFB < 5s with streaming.FullDraftsUpSync.Implementation order
MoreAvailableordering.References
feat(activesync): adaptive Sync response time budgetdoc/todo.md— request/response pipeline, Sync data structures