Skip to content

Commit 2dee620

Browse files
fix: handle missing user theme_preference on sign in (#91)
coder/coder#16564 has the Coder server no longer send the `theme_preference` field in a `/api/v2/user` response. The Swift `JSONDecoder` requires that a missing field be explicitly marked as optional, else the deserialization fails. To make it less likely this happens again, we'll only require `id` and `username` be present. We'll do the same for the other SDK types and only require the minimum fields the app needs be present. This PR also improves the error message on any decoding error: <img width="259" alt="Screenshot 2025-03-06 at 1 35 33 pm" src="https://github.com/user-attachments/assets/0fef147c-29ad-41bf-9aff-29651ff6b796" />
1 parent ae51d0e commit 2dee620

File tree

5 files changed

+29
-106
lines changed

5 files changed

+29
-106
lines changed

Coder Desktop/Coder DesktopTests/LoginFormTests.swift

+1-12
Original file line numberDiff line numberDiff line change
@@ -93,18 +93,7 @@ struct LoginTests {
9393

9494
let user = User(
9595
id: UUID(),
96-
username: "admin",
97-
avatar_url: "",
98-
name: "admin",
99-
100-
created_at: Date.now,
101-
updated_at: Date.now,
102-
last_seen_at: Date.now,
103-
status: "active",
104-
login_type: "none",
105-
theme_preference: "dark",
106-
organization_ids: [],
107-
roles: []
96+
username: "admin"
10897
)
10998

11099
try Mock(

Coder Desktop/CoderSDK/Client.swift

+22-5
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public struct Client {
4444
throw .network(error)
4545
}
4646
guard let httpResponse = resp as? HTTPURLResponse else {
47-
throw .unexpectedResponse(data)
47+
throw .unexpectedResponse(String(data: data, encoding: .utf8) ?? "<non-utf8 data>")
4848
}
4949
return HTTPResponse(resp: httpResponse, data: data, req: req)
5050
}
@@ -72,7 +72,7 @@ public struct Client {
7272

7373
func responseAsError(_ resp: HTTPResponse) -> ClientError {
7474
do {
75-
let body = try Client.decoder.decode(Response.self, from: resp.data)
75+
let body = try decode(Response.self, from: resp.data)
7676
let out = APIError(
7777
response: body,
7878
statusCode: resp.resp.statusCode,
@@ -81,7 +81,24 @@ public struct Client {
8181
)
8282
return .api(out)
8383
} catch {
84-
return .unexpectedResponse(resp.data.prefix(1024))
84+
return .unexpectedResponse(String(data: resp.data, encoding: .utf8) ?? "<non-utf8 data>")
85+
}
86+
}
87+
88+
// Wrapper around JSONDecoder.decode that displays useful error messages from `DecodingError`.
89+
func decode<T>(_: T.Type, from data: Data) throws(ClientError) -> T where T: Decodable {
90+
do {
91+
return try Client.decoder.decode(T.self, from: data)
92+
} catch let DecodingError.keyNotFound(_, context) {
93+
throw .unexpectedResponse("Key not found: \(context.debugDescription)")
94+
} catch let DecodingError.valueNotFound(_, context) {
95+
throw .unexpectedResponse("Value not found: \(context.debugDescription)")
96+
} catch let DecodingError.typeMismatch(_, context) {
97+
throw .unexpectedResponse("Type mismatch: \(context.debugDescription)")
98+
} catch let DecodingError.dataCorrupted(context) {
99+
throw .unexpectedResponse("Data corrupted: \(context.debugDescription)")
100+
} catch {
101+
throw .unexpectedResponse(String(data: data.prefix(1024), encoding: .utf8) ?? "<non-utf8 data>")
85102
}
86103
}
87104
}
@@ -119,7 +136,7 @@ public struct FieldValidation: Decodable, Sendable {
119136
public enum ClientError: Error {
120137
case api(APIError)
121138
case network(any Error)
122-
case unexpectedResponse(Data)
139+
case unexpectedResponse(String)
123140
case encodeFailure(any Error)
124141

125142
public var description: String {
@@ -129,7 +146,7 @@ public enum ClientError: Error {
129146
case let .network(error):
130147
error.localizedDescription
131148
case let .unexpectedResponse(data):
132-
"Unexpected or non HTTP response: \(data)"
149+
"Unexpected response: \(data)"
133150
case let .encodeFailure(error):
134151
"Failed to encode body: \(error.localizedDescription)"
135152
}

Coder Desktop/CoderSDK/Deployment.swift

+2-14
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,12 @@ public extension Client {
66
guard res.resp.statusCode == 200 else {
77
throw responseAsError(res)
88
}
9-
do {
10-
return try Client.decoder.decode(BuildInfoResponse.self, from: res.data)
11-
} catch {
12-
throw .unexpectedResponse(res.data.prefix(1024))
13-
}
9+
return try decode(BuildInfoResponse.self, from: res.data)
1410
}
1511
}
1612

17-
public struct BuildInfoResponse: Encodable, Decodable, Equatable, Sendable {
18-
public let external_url: String
13+
public struct BuildInfoResponse: Codable, Equatable, Sendable {
1914
public let version: String
20-
public let dashboard_url: String
21-
public let telemetry: Bool
22-
public let workspace_proxy: Bool
23-
public let agent_api_version: String
24-
public let provisioner_api_version: String
25-
public let upgrade_message: String
26-
public let deployment_id: String
2715

2816
// `version` in the form `[0-9]+.[0-9]+.[0-9]+`
2917
public var semver: String? {

Coder Desktop/CoderSDK/User.swift

+2-51
Original file line numberDiff line numberDiff line change
@@ -6,68 +6,19 @@ public extension Client {
66
guard res.resp.statusCode == 200 else {
77
throw responseAsError(res)
88
}
9-
do {
10-
return try Client.decoder.decode(User.self, from: res.data)
11-
} catch {
12-
throw .unexpectedResponse(res.data.prefix(1024))
13-
}
9+
return try decode(User.self, from: res.data)
1410
}
1511
}
1612

1713
public struct User: Encodable, Decodable, Equatable, Sendable {
1814
public let id: UUID
1915
public let username: String
20-
public let avatar_url: String
21-
public let name: String
22-
public let email: String
23-
public let created_at: Date
24-
public let updated_at: Date
25-
public let last_seen_at: Date
26-
public let status: String
27-
public let login_type: String
28-
public let theme_preference: String
29-
public let organization_ids: [UUID]
30-
public let roles: [Role]
3116

3217
public init(
3318
id: UUID,
34-
username: String,
35-
avatar_url: String,
36-
name: String,
37-
email: String,
38-
created_at: Date,
39-
updated_at: Date,
40-
last_seen_at: Date,
41-
status: String,
42-
login_type: String,
43-
theme_preference: String,
44-
organization_ids: [UUID],
45-
roles: [Role]
19+
username: String
4620
) {
4721
self.id = id
4822
self.username = username
49-
self.avatar_url = avatar_url
50-
self.name = name
51-
self.email = email
52-
self.created_at = created_at
53-
self.updated_at = updated_at
54-
self.last_seen_at = last_seen_at
55-
self.status = status
56-
self.login_type = login_type
57-
self.theme_preference = theme_preference
58-
self.organization_ids = organization_ids
59-
self.roles = roles
60-
}
61-
}
62-
63-
public struct Role: Encodable, Decodable, Equatable, Sendable {
64-
public let name: String
65-
public let display_name: String
66-
public let organization_id: UUID?
67-
68-
public init(name: String, display_name: String, organization_id: UUID?) {
69-
self.name = name
70-
self.display_name = display_name
71-
self.organization_id = organization_id
7223
}
7324
}

Coder Desktop/CoderSDKTests/CoderSDKTests.swift

+2-24
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,9 @@ import Testing
77
struct CoderSDKTests {
88
@Test
99
func user() async throws {
10-
let now = Date.now
1110
let user = User(
1211
id: UUID(),
13-
username: "johndoe",
14-
avatar_url: "https://example.com/img.png",
15-
name: "John Doe",
16-
17-
created_at: now,
18-
updated_at: now,
19-
last_seen_at: now,
20-
status: "active",
21-
login_type: "email",
22-
theme_preference: "dark",
23-
organization_ids: [UUID()],
24-
roles: [
25-
Role(name: "user", display_name: "User", organization_id: UUID()),
26-
]
12+
username: "johndoe"
2713
)
2814

2915
let url = URL(string: "https://example.com")!
@@ -50,15 +36,7 @@ struct CoderSDKTests {
5036
@Test
5137
func buildInfo() async throws {
5238
let buildInfo = BuildInfoResponse(
53-
external_url: "https://example.com",
54-
version: "v2.18.2-devel+630fd7c0a",
55-
dashboard_url: "https://example.com/dashboard",
56-
telemetry: true,
57-
workspace_proxy: false,
58-
agent_api_version: "1.0",
59-
provisioner_api_version: "1.2",
60-
upgrade_message: "foo",
61-
deployment_id: UUID().uuidString
39+
version: "v2.18.2-devel+630fd7c0a"
6240
)
6341

6442
let url = URL(string: "https://example.com")!

0 commit comments

Comments
 (0)