Summary
Three latent issues in the backup save path can produce silent failures or corrupt backups under real-world conditions. Backups are a data-integrity feature — they need to be 100% bulletproof before users rely on them.
The current pipeline is pure pass-through (decode base64 → atomic write), which is correct in spirit, but three guardrails are missing.
Issues
1. WebSocket frame size limit (highest impact)
Z2MWebSocketClient does not set maximumMessageSize on its URLSessionWebSocketTask. The default is 1 MB.
A populated Z2M install (30+ devices, months of state, rotated config backups) easily produces a 3–10 MB backup zip. Once base64-encoded inside the JSON envelope (+33% inflation), the WS frame crosses 1 MB and the receive call fails — the user sees a generic error or a disconnect, with no way to retry into a working state.
Fix: raise maximumMessageSize on connect (e.g. 64 MB).
2. Strict base64 decoding
BackupView.saveBackup calls Data(base64Encoded: base64) with default options, which rejects any whitespace or newline characters. Some MQTT/WS serializers insert line breaks in long strings. If that ever happens upstream, the decode silently returns nil and the user sees "Invalid base64 zip" with no actionable info.
Fix: pass .ignoreUnknownCharacters.
3. No post-write integrity check
After writing the zip, we report success based solely on the file existing. A truncated, empty, or corrupt file would still show "Backup ready" — and the user only discovers it when restore fails months later.
Fix: after data.write, verify:
data.count > 0
- First 4 bytes are
50 4B 03 04 (ZIP local file header magic)
If either fails, surface a failed status and delete the partial file.
Code references
Shellbee/Features/Settings/Backup/BackupView.swift — saveBackup(base64:) and triggerBackup()
Shellbee/Core/Networking/Z2MWebSocketClient.swift — WS task configuration (no maximumMessageSize set today)
Shellbee/Core/Store/AppStore.swift — backupResponseHandler dispatch (line ~204)
Test plan
- Add a unit test that feeds
saveBackup a known-good base64 zip → asserts file exists, size matches, magic bytes correct.
- Add a unit test for the corruption path: truncated base64, base64 with embedded newlines (passes with
.ignoreUnknownCharacters), and a non-zip payload — assert each path either succeeds or returns a failed status, never a false "ready".
- Manual: drive
seeder to emit a backup >1 MB (inflate fixture state) and confirm Shellbee receives it without disconnect after the maximumMessageSize change.
Summary
Three latent issues in the backup save path can produce silent failures or corrupt backups under real-world conditions. Backups are a data-integrity feature — they need to be 100% bulletproof before users rely on them.
The current pipeline is pure pass-through (decode base64 → atomic write), which is correct in spirit, but three guardrails are missing.
Issues
1. WebSocket frame size limit (highest impact)
Z2MWebSocketClientdoes not setmaximumMessageSizeon itsURLSessionWebSocketTask. The default is 1 MB.A populated Z2M install (30+ devices, months of state, rotated config backups) easily produces a 3–10 MB backup zip. Once base64-encoded inside the JSON envelope (+33% inflation), the WS frame crosses 1 MB and the receive call fails — the user sees a generic error or a disconnect, with no way to retry into a working state.
Fix: raise
maximumMessageSizeon connect (e.g. 64 MB).2. Strict base64 decoding
BackupView.saveBackupcallsData(base64Encoded: base64)with default options, which rejects any whitespace or newline characters. Some MQTT/WS serializers insert line breaks in long strings. If that ever happens upstream, the decode silently returns nil and the user sees "Invalid base64 zip" with no actionable info.Fix: pass
.ignoreUnknownCharacters.3. No post-write integrity check
After writing the zip, we report success based solely on the file existing. A truncated, empty, or corrupt file would still show "Backup ready" — and the user only discovers it when restore fails months later.
Fix: after
data.write, verify:data.count > 050 4B 03 04(ZIP local file header magic)If either fails, surface a
failedstatus and delete the partial file.Code references
Shellbee/Features/Settings/Backup/BackupView.swift—saveBackup(base64:)andtriggerBackup()Shellbee/Core/Networking/Z2MWebSocketClient.swift— WS task configuration (nomaximumMessageSizeset today)Shellbee/Core/Store/AppStore.swift—backupResponseHandlerdispatch (line ~204)Test plan
saveBackupa known-good base64 zip → asserts file exists, size matches, magic bytes correct..ignoreUnknownCharacters), and a non-zip payload — assert each path either succeeds or returns afailedstatus, never a false "ready".seederto emit a backup >1 MB (inflate fixture state) and confirm Shellbee receives it without disconnect after themaximumMessageSizechange.