From c1c998ab40d3707fcf557bb7ac6c1c0db6718e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Mon, 22 Jun 2026 19:24:33 +0200 Subject: [PATCH 1/2] Sanitize Nightscout token to prevent WebSocket crash on launch Strip whitespace, newlines, and control characters from the token before storing it and before opening the WebSocket. A stray character (typically pasted in) produced an invalid percent-encoded query in Socket.IO's URL builder, which traps on iOS 26 and crashed the app on startup. Existing saved tokens are sanitized defensively at connect time. --- .../Controllers/Nightscout/NightscoutSocketManager.swift | 4 +++- LoopFollow/Helpers/NightscoutUtils.swift | 9 +++++++++ LoopFollow/Nightscout/NightscoutSettingsViewModel.swift | 2 +- 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift index 2508c94e2..019baafbc 100644 --- a/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift +++ b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift @@ -41,7 +41,9 @@ class NightscoutSocketManager { } let url = Storage.shared.url.value - let token = Storage.shared.token.value + // Sanitize defensively: tokens saved before this fix may still hold a stray + // whitespace/control char that crashes Socket.IO's URL builder on iOS 26. + let token = NightscoutUtils.sanitizeToken(Storage.shared.token.value) guard !url.isEmpty else { disconnect() diff --git a/LoopFollow/Helpers/NightscoutUtils.swift b/LoopFollow/Helpers/NightscoutUtils.swift index 04c5ff14b..8efd23ab6 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -162,6 +162,15 @@ class NightscoutUtils { return request } + // Strip whitespace/newlines/control characters that can sneak in via paste. + // A raw one breaks WebSocket connect (invalid percent-encoded query traps on iOS 26). + static func sanitizeToken(_ token: String) -> String { + token.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..dc6201185 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.sanitizeToken(newValue) triggerCheckStatus() } } From 97fd3b2dc5e8859bcb9bd0923e2067b61a185cb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonas=20Bj=C3=B6rkert?= Date: Tue, 23 Jun 2026 13:54:10 +0200 Subject: [PATCH 2/2] Sanitize URL entry path, not just token, to prevent WebSocket crash --- .../Controllers/Nightscout/NightscoutSocketManager.swift | 6 +++--- LoopFollow/Helpers/NightscoutUtils.swift | 8 +++++--- LoopFollow/Nightscout/NightscoutSettingsViewModel.swift | 8 +++++++- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift index 019baafbc..6db744aa9 100644 --- a/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift +++ b/LoopFollow/Controllers/Nightscout/NightscoutSocketManager.swift @@ -40,10 +40,10 @@ class NightscoutSocketManager { return } - let url = Storage.shared.url.value - // Sanitize defensively: tokens saved before this fix may still hold a stray + // 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 token = NightscoutUtils.sanitizeToken(Storage.shared.token.value) + 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 8efd23ab6..88b1995b7 100644 --- a/LoopFollow/Helpers/NightscoutUtils.swift +++ b/LoopFollow/Helpers/NightscoutUtils.swift @@ -163,9 +163,11 @@ class NightscoutUtils { } // Strip whitespace/newlines/control characters that can sneak in via paste. - // A raw one breaks WebSocket connect (invalid percent-encoded query traps on iOS 26). - static func sanitizeToken(_ token: String) -> String { - token.unicodeScalars + // 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() diff --git a/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift b/LoopFollow/Nightscout/NightscoutSettingsViewModel.swift index dc6201185..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 = NightscoutUtils.sanitizeToken(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 {