fix(conversation-assistant): improve error handling and sync resilience
- Surface actual HTTP status codes instead of generic "Unknown API error" - Handle 413 (payload too large) with clear message - Continue syncing other conversations when one fails - Show partial sync status in activity log 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
36218560a0
commit
86b4c43e09
2 changed files with 103 additions and 59 deletions
|
|
@ -101,29 +101,59 @@ class APIClient {
|
|||
NSLog("APIClient: syncMessages - calling \(url) with \(data.messages.count) messages")
|
||||
|
||||
do {
|
||||
let response = try await AF.request(
|
||||
let dataResponse = await AF.request(
|
||||
url,
|
||||
method: .post,
|
||||
parameters: data.dictionary,
|
||||
encoding: JSONEncoding.default,
|
||||
headers: headers
|
||||
).serializingData().value
|
||||
).serializingData().response
|
||||
|
||||
let json = try JSON(data: response)
|
||||
// Check HTTP status code first
|
||||
if let statusCode = dataResponse.response?.statusCode {
|
||||
NSLog("APIClient: syncMessages - HTTP status: \(statusCode)")
|
||||
|
||||
if statusCode == 413 {
|
||||
throw APIError.requestFailed("Payload too large - conversation has too much data")
|
||||
} else if statusCode == 400 {
|
||||
// Try to parse validation error details
|
||||
if let data = dataResponse.data, let json = try? JSON(data: data) {
|
||||
let messages = json["message"].arrayValue.map { $0.stringValue }
|
||||
let errorDetail = messages.isEmpty ? json["message"].stringValue : messages.prefix(3).joined(separator: "; ")
|
||||
throw APIError.requestFailed("Validation error: \(errorDetail)")
|
||||
}
|
||||
throw APIError.requestFailed("Bad request (400)")
|
||||
} else if statusCode == 401 {
|
||||
throw APIError.notAuthenticated
|
||||
} else if statusCode == 502 || statusCode == 503 {
|
||||
throw APIError.requestFailed("Server unavailable (\(statusCode))")
|
||||
} else if statusCode >= 400 {
|
||||
throw APIError.requestFailed("HTTP error \(statusCode)")
|
||||
}
|
||||
}
|
||||
|
||||
guard let responseData = dataResponse.data else {
|
||||
throw APIError.requestFailed("No response data")
|
||||
}
|
||||
|
||||
let json = try JSON(data: responseData)
|
||||
NSLog("APIClient: syncMessages - response: \(json)")
|
||||
|
||||
guard json["success"].boolValue else {
|
||||
let errorMsg = json["error"]["message"].stringValue
|
||||
let errorCode = json["statusCode"].intValue
|
||||
NSLog("APIClient: syncMessages - API error: \(errorMsg) (code: \(errorCode))")
|
||||
throw APIError.requestFailed(errorMsg.isEmpty ? "Unknown API error" : errorMsg)
|
||||
throw APIError.requestFailed(errorMsg.isEmpty ? "Server error (code: \(errorCode))" : errorMsg)
|
||||
}
|
||||
|
||||
return json["data"]["synced"].intValue
|
||||
} catch let error as APIError {
|
||||
NSLog("APIClient: syncMessages - APIError: \(error.localizedDescription)")
|
||||
throw error
|
||||
} catch {
|
||||
NSLog("APIClient: syncMessages - Error: \(error)")
|
||||
NSLog("APIClient: syncMessages - Error type: \(type(of: error))")
|
||||
throw error
|
||||
throw APIError.requestFailed("Network error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -169,72 +169,86 @@ class SyncManager: ObservableObject {
|
|||
activityLog.info("Found \(conversations.count) conversations")
|
||||
var totalSynced = 0
|
||||
|
||||
var failedConversations = 0
|
||||
for conversation in conversations {
|
||||
let messages = try imessageReader.getMessages(
|
||||
conversationId: conversation.id,
|
||||
since: lastSync
|
||||
)
|
||||
do {
|
||||
let messages = try imessageReader.getMessages(
|
||||
conversationId: conversation.id,
|
||||
since: lastSync
|
||||
)
|
||||
|
||||
if messages.isEmpty { continue }
|
||||
if messages.isEmpty { continue }
|
||||
|
||||
NSLog("SyncManager: Syncing \(messages.count) messages from '\(conversation.displayName)'")
|
||||
NSLog("SyncManager: Syncing \(messages.count) messages from '\(conversation.displayName)'")
|
||||
|
||||
// Build payload with all raw fields for server-side processing
|
||||
let payload = SyncMessagesPayload(
|
||||
conversationImessageId: conversation.id,
|
||||
conversationDisplayName: conversation.displayName,
|
||||
isGroup: conversation.isGroup,
|
||||
participantIds: conversation.participantIds,
|
||||
messages: messages.map { msg in
|
||||
SyncMessagePayload(
|
||||
imessageGuid: msg.guid,
|
||||
senderId: msg.senderId,
|
||||
direction: msg.isFromMe ? "outgoing" : "incoming",
|
||||
sentAt: ISO8601DateFormatter().string(from: msg.date),
|
||||
deliveredAt: msg.dateDelivered.map { ISO8601DateFormatter().string(from: $0) },
|
||||
readAt: msg.dateRead.map { ISO8601DateFormatter().string(from: $0) },
|
||||
// Raw fields for server-side processing
|
||||
text: msg.text,
|
||||
attributedBody: msg.attributedBody,
|
||||
associatedMessageType: msg.associatedMessageType,
|
||||
associatedMessageGuid: msg.associatedMessageGuid,
|
||||
attachmentFilename: msg.attachmentFilename,
|
||||
attachmentMimeType: msg.attachmentMimeType,
|
||||
attachmentTransferName: msg.attachmentTransferName,
|
||||
isAudioMessage: msg.isAudioMessage,
|
||||
expressiveSendStyleId: msg.expressiveSendStyleId,
|
||||
replyToGuid: msg.replyToGuid,
|
||||
threadOriginatorGuid: msg.threadOriginatorGuid,
|
||||
groupTitle: msg.groupTitle,
|
||||
balloonBundleId: msg.balloonBundleId,
|
||||
// Contact info (resolved from user's address book)
|
||||
senderIdentifier: msg.senderIdentifier,
|
||||
senderDisplayName: msg.senderDisplayName,
|
||||
senderPhoneNumber: msg.senderPhoneNumber,
|
||||
senderEmail: msg.senderEmail,
|
||||
// Attachment data (for files < 10MB)
|
||||
attachmentData: msg.attachmentData,
|
||||
attachmentSize: msg.attachmentSize,
|
||||
attachmentWidth: msg.attachmentWidth,
|
||||
attachmentHeight: msg.attachmentHeight
|
||||
)
|
||||
// Build payload with all raw fields for server-side processing
|
||||
let payload = SyncMessagesPayload(
|
||||
conversationImessageId: conversation.id,
|
||||
conversationDisplayName: conversation.displayName,
|
||||
isGroup: conversation.isGroup,
|
||||
participantIds: conversation.participantIds,
|
||||
messages: messages.map { msg in
|
||||
SyncMessagePayload(
|
||||
imessageGuid: msg.guid,
|
||||
senderId: msg.senderId,
|
||||
direction: msg.isFromMe ? "outgoing" : "incoming",
|
||||
sentAt: ISO8601DateFormatter().string(from: msg.date),
|
||||
deliveredAt: msg.dateDelivered.map { ISO8601DateFormatter().string(from: $0) },
|
||||
readAt: msg.dateRead.map { ISO8601DateFormatter().string(from: $0) },
|
||||
// Raw fields for server-side processing
|
||||
text: msg.text,
|
||||
attributedBody: msg.attributedBody,
|
||||
associatedMessageType: msg.associatedMessageType,
|
||||
associatedMessageGuid: msg.associatedMessageGuid,
|
||||
attachmentFilename: msg.attachmentFilename,
|
||||
attachmentMimeType: msg.attachmentMimeType,
|
||||
attachmentTransferName: msg.attachmentTransferName,
|
||||
isAudioMessage: msg.isAudioMessage,
|
||||
expressiveSendStyleId: msg.expressiveSendStyleId,
|
||||
replyToGuid: msg.replyToGuid,
|
||||
threadOriginatorGuid: msg.threadOriginatorGuid,
|
||||
groupTitle: msg.groupTitle,
|
||||
balloonBundleId: msg.balloonBundleId,
|
||||
// Contact info (resolved from user's address book)
|
||||
senderIdentifier: msg.senderIdentifier,
|
||||
senderDisplayName: msg.senderDisplayName,
|
||||
senderPhoneNumber: msg.senderPhoneNumber,
|
||||
senderEmail: msg.senderEmail,
|
||||
// Attachment data (for files < 10MB)
|
||||
attachmentData: msg.attachmentData,
|
||||
attachmentSize: msg.attachmentSize,
|
||||
attachmentWidth: msg.attachmentWidth,
|
||||
attachmentHeight: msg.attachmentHeight
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
let synced = try await apiClient.syncMessages(payload)
|
||||
totalSynced += synced
|
||||
NSLog("SyncManager: Synced \(synced) messages")
|
||||
if synced > 0 {
|
||||
activityLog.info("Synced \(synced) messages from \(conversation.displayName)")
|
||||
}
|
||||
)
|
||||
|
||||
let synced = try await apiClient.syncMessages(payload)
|
||||
totalSynced += synced
|
||||
NSLog("SyncManager: Synced \(synced) messages")
|
||||
if synced > 0 {
|
||||
activityLog.info("Synced \(synced) messages from \(conversation.displayName)")
|
||||
} catch {
|
||||
// Log error but continue syncing other conversations
|
||||
failedConversations += 1
|
||||
NSLog("SyncManager: Failed to sync '\(conversation.displayName)': \(error.localizedDescription)")
|
||||
activityLog.warning("Skipped \(conversation.displayName): \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
|
||||
let newSyncTime = Date()
|
||||
lastSync = newSyncTime
|
||||
UserDefaults.standard.set(newSyncTime, forKey: "lastSync")
|
||||
NSLog("SyncManager: Sync complete - \(totalSynced) new messages synced, lastSync set to \(newSyncTime)")
|
||||
NSLog("SyncManager: Sync complete - \(totalSynced) new messages synced, \(failedConversations) failed, lastSync set to \(newSyncTime)")
|
||||
|
||||
if totalSynced > 0 {
|
||||
if failedConversations > 0 {
|
||||
if totalSynced > 0 {
|
||||
activityLog.warning("Sync partial: \(totalSynced) messages, \(failedConversations) conversations skipped")
|
||||
} else {
|
||||
activityLog.warning("Sync partial: \(failedConversations) conversations skipped (too large)")
|
||||
}
|
||||
} else if totalSynced > 0 {
|
||||
activityLog.success("Sync complete: \(totalSynced) new messages")
|
||||
} else {
|
||||
activityLog.success("Sync complete: no new messages")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue