diff --git a/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift index 2508c94e2..6db744aa9 100644 --- a/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift +++ b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift @@ -40,8 +40,10 @@ class NightscoutSocketManager { return } - let url = Storage.shared.url.value - let token = Storage.shared.token.value + // Sanitize defensively: values saved before this fix may still hold a stray + // whitespace/control char that crashes Socket.IO's URL builder on iOS 26. + let url = NightscoutUtils.sanitizeConnectionInput(Storage.shared.url.value) + let token = NightscoutUtils.sanitizeConnectionInput(Storage.shared.token.value) guard !url.isEmpty else { disconnect() diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index 04c5ff14b..88b1995b7 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -162,6 +162,17 @@ class NightscoutUtils { return request } + // Strip whitespace/newlines/control characters that can sneak in via paste. + // Neither a URL nor a token may legally contain them, and a stray one breaks + // WebSocket connect (invalid percent-encoded query traps on iOS 26) or makes + // URL parsing fall back to a lossy cleanup that mangles the address. + static func sanitizeConnectionInput(_ input: String) -> String { + input.unicodeScalars + .filter { !CharacterSet.whitespacesAndNewlines.contains($0) && !CharacterSet.controlCharacters.contains($0) } + .map(String.init) + .joined() + } + static func constructURL(baseURL: String, token: String?, endpoint: String, parameters: [String: String]) -> URL? { var components = URLComponents(string: baseURL) components?.path = endpoint diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index 559a12916..f15a5d309 100644 --- a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift +++ b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift @@ -27,7 +27,7 @@ class NightscoutSettingsViewModel: ObservableObject { @Published var nightscoutToken: String = Storage.shared.token.value { willSet { if newValue != nightscoutToken { - Storage.shared.token.value = newValue + Storage.shared.token.value = NightscoutUtils.sanitizeConnectionInput(newValue) triggerCheckStatus() } } @@ -93,6 +93,12 @@ class NightscoutSettingsViewModel: ObservableObject { } func processURL(_ value: String) { + // Strip whitespace/newlines/control chars first. A URL can't legally contain + // them, and a stray one (e.g. a trailing newline from a paste) otherwise makes + // URLComponents parsing fail and falls through to the lossy fallback below, + // mangling a URL-with-embedded-token into an invalid address. + let value = NightscoutUtils.sanitizeConnectionInput(value) + var useTokenUrl = false if let urlComponents = URLComponents(string: value), let queryItems = urlComponents.queryItems {