diff --git a/swift-sdk/Internal/HealthMonitor.swift b/swift-sdk/Internal/HealthMonitor.swift index 909e1000a..0f244f67b 100644 --- a/swift-sdk/Internal/HealthMonitor.swift +++ b/swift-sdk/Internal/HealthMonitor.swift @@ -94,7 +94,12 @@ class HealthMonitor { let currentDate = dateProvider.currentDate let apiCallRequest = apiCallRequest.addingCreatedAt(currentDate) if let urlRequest = apiCallRequest.convertToURLRequest(sentAt: currentDate) { + ITBInfo("Attempting to send failed-to-schedule request directly for path: '\(apiCallRequest.getPath())'") _ = RequestSender.sendRequest(urlRequest, usingSession: networkSession) + } else { + let endpoint = apiCallRequest.endpoint + let path = apiCallRequest.getPath() + ITBError("Failed to convert to URL request in health monitor - endpoint: '\(endpoint)', path: '\(path)'") } onError() } diff --git a/swift-sdk/Internal/InternalIterableAPI.swift b/swift-sdk/Internal/InternalIterableAPI.swift index 6469095d4..60f806172 100644 --- a/swift-sdk/Internal/InternalIterableAPI.swift +++ b/swift-sdk/Internal/InternalIterableAPI.swift @@ -964,7 +964,30 @@ final class InternalIterableAPI: NSObject, PushTrackerProtocol, AuthProvider { private static func setApiEndpoint(apiEndPointOverride: String?, config: IterableConfig) -> String { let apiEndPoint = config.dataRegion - return apiEndPointOverride ?? apiEndPoint + let endpoint = apiEndPointOverride ?? apiEndPoint + + // Sanitize and validate endpoint + let sanitized = endpoint.trimmingCharacters(in: .whitespacesAndNewlines) + + // Validate endpoint is a valid URL + if let url = URL(string: sanitized) { + if url.scheme == nil || url.host == nil { + ITBError("Invalid API endpoint - missing scheme or host: '\(sanitized)'") + } + } else { + ITBError("Invalid API endpoint - cannot create URL from: '\(sanitized)'") + } + + // Check for common issues + if sanitized != endpoint { + ITBError("API endpoint contained whitespace, trimmed from '\(endpoint)' to '\(sanitized)'") + } + + if sanitized.isEmpty { + ITBError("API endpoint is empty after sanitization") + } + + return sanitized } init(apiKey: String, diff --git a/swift-sdk/Internal/IterableAPICallTaskProcessor.swift b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift index 019912ead..344d963aa 100644 --- a/swift-sdk/Internal/IterableAPICallTaskProcessor.swift +++ b/swift-sdk/Internal/IterableAPICallTaskProcessor.swift @@ -22,7 +22,11 @@ struct IterableAPICallTaskProcessor: IterableTaskProcessor { let iterableRequest = decodedIterableRequest.addingCreatedAt(task.scheduledAt) guard let urlRequest = iterableRequest.convertToURLRequest(sentAt: dateProvider.currentDate, processorType: .offline) else { - return IterableTaskError.createErroredFuture(reason: "could not convert to url request") + let endpoint = decodedIterableRequest.endpoint + let path = decodedIterableRequest.getPath() + let errorMessage = "Could not convert to URL request - endpoint: '\(endpoint)', path: '\(path)'" + ITBError(errorMessage) + return IterableTaskError.createErroredFuture(reason: errorMessage) } let result = Fulfill() @@ -47,10 +51,38 @@ struct IterableAPICallTaskProcessor: IterableTaskProcessor { private let dateProvider: DateProviderProtocol private static func isNetworkUnavailable(sendRequestError: SendRequestError) -> Bool { + // Check for NSURLError codes that indicate network issues + if let nsError = sendRequestError.originalError as? NSError { + if nsError.domain == NSURLErrorDomain { + let networkErrorCodes: Set = [ + NSURLErrorNotConnectedToInternet, // -1009 + NSURLErrorNetworkConnectionLost, // -1005 + NSURLErrorTimedOut, // -1001 + NSURLErrorCannotConnectToHost, // -1004 + NSURLErrorDNSLookupFailed, // -1006 + NSURLErrorDataNotAllowed, // -1020 (cellular data disabled) + NSURLErrorInternationalRoamingOff // -1018 + ] + if networkErrorCodes.contains(nsError.code) { + ITBInfo("Network error detected: code=\(nsError.code), description=\(nsError.localizedDescription)") + return true + } + } + } + + // Fallback to string check for other network-related errors if let originalError = sendRequestError.originalError { - return originalError.localizedDescription.lowercased().contains("offline") - } else { - return false + let description = originalError.localizedDescription.lowercased() + let isNetworkError = description.contains("offline") || + description.contains("network") || + description.contains("internet") || + description.contains("connection") + if isNetworkError { + ITBInfo("Network error detected via description: \(originalError.localizedDescription)") + } + return isNetworkError } + + return false } } diff --git a/swift-sdk/Internal/IterableRequest.swift b/swift-sdk/Internal/IterableRequest.swift index 9b33019ca..a3897498f 100644 --- a/swift-sdk/Internal/IterableRequest.swift +++ b/swift-sdk/Internal/IterableRequest.swift @@ -53,6 +53,15 @@ extension IterableRequest: Codable { } } + func getPath() -> String { + switch self { + case .get(let request): + return request.path + case .post(let request): + return request.path + } + } + private static let requestTypeGet = "get" private static let requestTypePost = "post" } diff --git a/swift-sdk/Internal/IterableRequestUtil.swift b/swift-sdk/Internal/IterableRequestUtil.swift index 2a6ba98cb..c1033f188 100644 --- a/swift-sdk/Internal/IterableRequestUtil.swift +++ b/swift-sdk/Internal/IterableRequestUtil.swift @@ -13,6 +13,7 @@ struct IterableRequestUtil { headers: [String: String]? = nil, args: [String: String]? = nil) -> URLRequest? { guard let url = getUrlComponents(forApiEndPoint: apiEndPoint, path: path, args: args)?.url else { + ITBError("Failed to create GET request URL") return nil } @@ -53,6 +54,7 @@ struct IterableRequestUtil { args: [String: String]? = nil, body: Data? = nil) -> URLRequest? { guard let url = getUrlComponents(forApiEndPoint: apiEndPoint, path: path, args: args)?.url else { + ITBError("Failed to create POST request URL") return nil } @@ -88,6 +90,7 @@ struct IterableRequestUtil { let endPointCombined = pathCombine(path1: apiEndPoint, path2: path) guard var components = URLComponents(string: "\(endPointCombined)") else { + ITBError("Failed to create URLComponents - apiEndPoint: '\(apiEndPoint)', path: '\(path)', combined: '\(endPointCombined)'") return nil } diff --git a/swift-sdk/Internal/api-client/ApiClient.swift b/swift-sdk/Internal/api-client/ApiClient.swift index c7dd3e44c..f513ad5af 100644 --- a/swift-sdk/Internal/api-client/ApiClient.swift +++ b/swift-sdk/Internal/api-client/ApiClient.swift @@ -84,7 +84,10 @@ class ApiClient { func send(iterableRequest: IterableRequest) -> Pending { guard let urlRequest = convertToURLRequest(iterableRequest: iterableRequest) else { - return SendRequestError.createErroredFuture() + let path = iterableRequest.getPath() + let errorMessage = "Failed to create URL request for endpoint: '\(endpoint)', path: '\(path)'" + ITBError(errorMessage) + return SendRequestError.createErroredFuture(reason: errorMessage) } return RequestSender.sendRequest(urlRequest, usingSession: networkSession) @@ -92,7 +95,10 @@ class ApiClient { func sendWithoutCreatedAt(iterableRequest: IterableRequest) -> Pending { guard let urlRequest = convertToURLRequestWithoutCreatedAt(iterableRequest: iterableRequest) else { - return SendRequestError.createErroredFuture() + let path = iterableRequest.getPath() + let errorMessage = "Failed to create URL request for endpoint: '\(endpoint)', path: '\(path)'" + ITBError(errorMessage) + return SendRequestError.createErroredFuture(reason: errorMessage) } return RequestSender.sendRequest(urlRequest, usingSession: networkSession) @@ -100,7 +106,10 @@ class ApiClient { func send(iterableRequest: IterableRequest) -> Pending where T: Decodable { guard let urlRequest = convertToURLRequest(iterableRequest: iterableRequest) else { - return SendRequestError.createErroredFuture() + let path = iterableRequest.getPath() + let errorMessage = "Failed to create URL request for endpoint: '\(endpoint)', path: '\(path)'" + ITBError(errorMessage) + return SendRequestError.createErroredFuture(reason: errorMessage) } return RequestSender.sendRequest(urlRequest, usingSession: networkSession)