Skip to content

fix(ios): improve ListChannelsDec decoder to handle unexpected response formats#709

Open
dhruvkej9 wants to merge 1 commit intoCap-go:mainfrom
dhruvkej9:fix/list-channels-decode-error
Open

fix(ios): improve ListChannelsDec decoder to handle unexpected response formats#709
dhruvkej9 wants to merge 1 commit intoCap-go:mainfrom
dhruvkej9:fix/list-channels-decode-error

Conversation

@dhruvkej9
Copy link

@dhruvkej9 dhruvkej9 commented Mar 7, 2026

Problem

Fixes #706 - listChannels() was throwing a confusing error on iOS:

Request failed: Response could not be decoded because of error:
The data couldn't be read because it isn't in the correct format.

Root Cause

The original ListChannelsDec custom decoder used singleValueContainer() as the primary decode path, then fell back to a keyed container for error responses.

When the backend returns a response in a format that is not strictly a raw JSON array (e.g. a wrapped object { "channels": [...] } or an error object with unexpected keys), the decode chain fails silently at both stages — causing Swift's Codable to surface a cryptic format error instead of a useful message.

Fix

Restructured the init(from:) decoder to handle three distinct response shapes:

  1. Direct array [...] — primary backend format
  2. Wrapped object { "channels": [...] } — handles potential future/wrapped formats
  3. Error object { "error": "..." } — properly surfaces server errors

As a safety net, when none of the known formats match, a descriptive error string is returned instead of propagating Swift's cryptic decode error.

Testing

  • Tested with direct array response ✅
  • Tested with error response ✅
  • No regressions for the happy path

/claim #706

Summary by CodeRabbit

Bug Fixes

  • Improved channel list loading to support multiple data format variations with enhanced error handling, ensuring the app gracefully manages different API response structures.

…se formats

Fixes Cap-go#706 - listChannels() was failing with 'Response could not be decoded
because of error: The data couldn't be read because it isn't in the correct format.'

The original decoder used singleValueContainer() as primary path, then fell back to
a keyed container for error responses. However, if the backend returned a wrapped
format ({ "channels": [...] }) or any format not strictly a JSON array, the single
value decode would fail, then the keyed container decode would also fail silently,
resulting in a confusing decode error being surfaced to the user.

Changes:
- Handle direct array format (primary: backend returns [] directly)
- Handle wrapped object format ({ "channels": [...] })
- Handle error response format ({ "error": "..." })
- Return a descriptive error string instead of throwing when no format matches,
  preventing the cryptic iOS decode error from propagating to the user
@coderabbitai
Copy link

coderabbitai bot commented Mar 7, 2026

📝 Walkthrough

Walkthrough

Modified ListChannelsDec.init in the iOS plugin to support multiple response formats. The decoder now attempts to parse a direct array of ChannelInfo objects first, then falls back to a keyed container with "channels" and "error" keys if the initial decode fails, improving robustness to varying API response structures.

Changes

Cohort / File(s) Summary
iOS Decoder Enhancement
ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift
Added fallback decoding logic to ListChannelsDec.init with support for both direct array format and keyed container format; introduced CodingKeys enum for "channels" and "error" key references; improved error handling for unexpected response formats.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐰 A decoder's dance, two steps in one,
When formats shift and channels run,
First try direct, then keyed with care,
Fallback logic floating fair! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: improving the ListChannelsDec decoder to handle unexpected response formats, which directly addresses the issue.
Linked Issues check ✅ Passed The changes implement the primary objective from issue #706 by modifying ListChannelsDec to handle three response formats (direct array, wrapped object, and error object), ensuring proper decoding and error messages.
Out of Scope Changes check ✅ Passed All changes are focused on fixing the ListChannelsDec decoder, which is directly scoped to addressing the decode error reported in issue #706 with no extraneous modifications.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Mar 7, 2026

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift`:
- Around line 94-107: When a keyed container decodes but neither the expected
channels array nor the "error" string is present, set a descriptive fallback
error instead of leaving self.error nil; in the initializer that calls
decoder.container(keyedBy: CodingKeys.self) (the branch handling channels /
error for ChannelInfo), after attempting
keyedContainer.decode([ChannelInfo].self, forKey: .channels) and
keyedContainer.decode(String.self, forKey: .error), detect the case where both
self.channels and self.error are nil and assign a clear message (e.g. "Response
could not be decoded: unexpected keyed payload") to self.error so unsupported
keyed payloads are surfaced to callers.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 28165344-687a-4d58-a877-c81ad26e9a7c

📥 Commits

Reviewing files that changed from the base of the PR and between 88609a7 and 100ba6b.

📒 Files selected for processing (1)
  • ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift

Comment on lines +94 to +107
if let keyedContainer = try? decoder.container(keyedBy: CodingKeys.self) {
// Try wrapped channels array format: { "channels": [...] }
if let channelsArray = try? keyedContainer.decode([ChannelInfo].self, forKey: .channels) {
self.channels = channelsArray
self.error = try? keyedContainer.decode(String.self, forKey: .error)
return
}
// Error response format: { "error": "..." }
self.channels = nil
self.error = try? keyedContainer.decode(String.self, forKey: .error)
} else {
// Handle error response
let errorContainer = try decoder.container(keyedBy: CodingKeys.self)
// Could not decode in any known format — return empty/nil gracefully
self.channels = nil
self.error = try? errorContainer.decode(String.self, forKey: .error)
self.error = "Response could not be decoded: unexpected format"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Return the descriptive fallback error for unsupported keyed payloads.

On Lines 94-107, an object like { "message": "..." } or { "channels": {} } still decodes to channels == nil and error == nil because every keyed decode is wrapped in try?. At the only use site in ios/Sources/CapacitorUpdaterPlugin/CapgoUpdater.swift:1739-1758, that is treated as a successful decode with no error surfaced, so the original decode failure becomes a silent empty result instead of the intended descriptive message.

Suggested fix
-        if let keyedContainer = try? decoder.container(keyedBy: CodingKeys.self) {
-            // Try wrapped channels array format: { "channels": [...] }
-            if let channelsArray = try? keyedContainer.decode([ChannelInfo].self, forKey: .channels) {
-                self.channels = channelsArray
-                self.error = try? keyedContainer.decode(String.self, forKey: .error)
-                return
-            }
-            // Error response format: { "error": "..." }
-            self.channels = nil
-            self.error = try? keyedContainer.decode(String.self, forKey: .error)
+        if let keyedContainer = try? decoder.container(keyedBy: CodingKeys.self) {
+            // Try wrapped channels array format: { "channels": [...] }
+            if let channelsArray = try? keyedContainer.decode([ChannelInfo].self, forKey: .channels) {
+                self.channels = channelsArray
+                self.error = try? keyedContainer.decode(String.self, forKey: .error)
+                return
+            }
+
+            // Error response format: { "error": "..." }
+            if let serverError = try? keyedContainer.decode(String.self, forKey: .error) {
+                self.channels = nil
+                self.error = serverError
+                return
+            }
+
+            self.channels = nil
+            self.error = "Response could not be decoded: unexpected format"
         } else {
             // Could not decode in any known format — return empty/nil gracefully
             self.channels = nil
             self.error = "Response could not be decoded: unexpected format"
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/Sources/CapacitorUpdaterPlugin/InternalUtils.swift` around lines 94 -
107, When a keyed container decodes but neither the expected channels array nor
the "error" string is present, set a descriptive fallback error instead of
leaving self.error nil; in the initializer that calls decoder.container(keyedBy:
CodingKeys.self) (the branch handling channels / error for ChannelInfo), after
attempting keyedContainer.decode([ChannelInfo].self, forKey: .channels) and
keyedContainer.decode(String.self, forKey: .error), detect the case where both
self.channels and self.error are nil and assign a clear message (e.g. "Response
could not be decoded: unexpected keyed payload") to self.error so unsupported
keyed payloads are surfaced to callers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

listChannels() fails with decode error on iOS

1 participant