Skip to content

Commit db3ea7d

Browse files
authored
fix: show login screen when token expires during workspace polling (#83)
- in fact we will now jump to the login screen for any error other than socket timeout because of an OS wake-up - this patch also contains a re-work of the REST API exception. Coder backend sends very detailed messages with the reason for the http calls to be rejected. We now un-marshall those responses and fill the exception system with better details.
1 parent 6f604bb commit db3ea7d

File tree

6 files changed

+197
-63
lines changed

6 files changed

+197
-63
lines changed

CHANGELOG.md

+8-3
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22

33
## Unreleased
44

5+
### Fixed
6+
7+
- login screen is shown instead of an empty list of workspaces when token expired
8+
59
## 0.1.4 - 2025-04-11
610

711
### Fixed
812

9-
- SSH connection to a Workspace is no longer established only once
10-
- authorization wizard automatically goes to a previous screen when an error is encountered during connection to Coder deployment
13+
- SSH connection to a Workspace is no longer established only once
14+
- authorization wizard automatically goes to a previous screen when an error is encountered during connection to Coder
15+
deployment
1116

1217
### Changed
1318

14-
- action buttons on the token input step were swapped to achieve better keyboard navigation
19+
- action buttons on the token input step were swapped to achieve better keyboard navigation
1520
- URI `project_path` query parameter was renamed to `folder`
1621

1722
## 0.1.3 - 2025-04-09

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

+18-10
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package com.coder.toolbox
22

33
import com.coder.toolbox.cli.CoderCLIManager
44
import com.coder.toolbox.sdk.CoderRestClient
5+
import com.coder.toolbox.sdk.ex.APIResponseException
56
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
67
import com.coder.toolbox.util.CoderProtocolHandler
78
import com.coder.toolbox.util.DialogUi
@@ -60,7 +61,7 @@ class CoderRemoteProvider(
6061
// If we have an error in the polling we store it here before going back to
6162
// sign-in page, so we can display it there. This is mainly because there
6263
// does not seem to be a mechanism to show errors on the environment list.
63-
private var pollError: Exception? = null
64+
private var errorBuffer = mutableListOf<Throwable>()
6465

6566
// On the first load, automatically log in if we can.
6667
private var firstRun = true
@@ -141,14 +142,21 @@ class CoderRemoteProvider(
141142
client.setupSession()
142143
} else {
143144
context.logger.error(ex, "workspace polling error encountered")
144-
pollError = ex
145+
errorBuffer.add(ex)
145146
logout()
146147
break
147148
}
149+
} catch (ex: APIResponseException) {
150+
context.logger.error(ex, "error in contacting ${client.url} while polling the available workspaces")
151+
errorBuffer.add(ex)
152+
logout()
153+
goToEnvironmentsPage()
154+
break
148155
} catch (ex: Exception) {
149156
context.logger.error(ex, "workspace polling error encountered")
150-
pollError = ex
157+
errorBuffer.add(ex)
151158
logout()
159+
goToEnvironmentsPage()
152160
break
153161
}
154162

@@ -300,15 +308,14 @@ class CoderRemoteProvider(
300308
if (client == null) {
301309
// When coming back to the application, authenticate immediately.
302310
val autologin = shouldDoAutoLogin()
303-
var autologinEx: Exception? = null
304311
context.secrets.lastToken.let { lastToken ->
305312
context.secrets.lastDeploymentURL.let { lastDeploymentURL ->
306313
if (autologin && lastDeploymentURL.isNotBlank() && (lastToken.isNotBlank() || !settings.requireTokenAuth)) {
307314
try {
308315
AuthWizardState.goToStep(WizardStep.LOGIN)
309316
return AuthWizardPage(context, true, ::onConnect)
310317
} catch (ex: Exception) {
311-
autologinEx = ex
318+
errorBuffer.add(ex)
312319
}
313320
}
314321
}
@@ -317,11 +324,12 @@ class CoderRemoteProvider(
317324

318325
// Login flow.
319326
val authWizard = AuthWizardPage(context, false, ::onConnect)
320-
// We might have tried and failed to automatically log in.
321-
autologinEx?.let { authWizard.notify("Error logging in", it) }
322327
// We might have navigated here due to a polling error.
323-
pollError?.let { authWizard.notify("Error fetching workspaces", it) }
324-
328+
errorBuffer.forEach {
329+
authWizard.notify("Error encountered", it)
330+
}
331+
// and now reset the errors, otherwise we show it every time on the screen
332+
errorBuffer.clear()
325333
return authWizard
326334
}
327335
return null
@@ -336,7 +344,7 @@ class CoderRemoteProvider(
336344
// Currently we always remember, but this could be made an option.
337345
context.secrets.rememberMe = true
338346
this.client = client
339-
pollError = null
347+
errorBuffer.clear()
340348
pollJob?.cancel()
341349
pollJob = poll(client, cli)
342350
goToEnvironmentsPage()

src/main/kotlin/com/coder/toolbox/sdk/CoderRestClient.kt

+69-11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import com.coder.toolbox.sdk.convertors.OSConverter
77
import com.coder.toolbox.sdk.convertors.UUIDConverter
88
import com.coder.toolbox.sdk.ex.APIResponseException
99
import com.coder.toolbox.sdk.v2.CoderV2RestFacade
10+
import com.coder.toolbox.sdk.v2.models.ApiErrorResponse
1011
import com.coder.toolbox.sdk.v2.models.BuildInfo
1112
import com.coder.toolbox.sdk.v2.models.CreateWorkspaceBuildRequest
1213
import com.coder.toolbox.sdk.v2.models.Template
@@ -24,6 +25,7 @@ import com.coder.toolbox.util.getOS
2425
import com.squareup.moshi.Moshi
2526
import okhttp3.Credentials
2627
import okhttp3.OkHttpClient
28+
import retrofit2.Response
2729
import retrofit2.Retrofit
2830
import retrofit2.converter.moshi.MoshiConverterFactory
2931
import java.net.HttpURLConnection
@@ -55,6 +57,7 @@ open class CoderRestClient(
5557
private val pluginVersion: String = "development",
5658
) {
5759
private val settings = context.settingsStore.readOnly()
60+
private lateinit var moshi: Moshi
5861
private lateinit var httpClient: OkHttpClient
5962
private lateinit var retroRestClient: CoderV2RestFacade
6063

@@ -66,7 +69,7 @@ open class CoderRestClient(
6669
}
6770

6871
fun setupSession() {
69-
val moshi =
72+
moshi =
7073
Moshi.Builder()
7174
.add(ArchConverter())
7275
.add(InstantConverter())
@@ -152,7 +155,7 @@ open class CoderRestClient(
152155
suspend fun me(): User {
153156
val userResponse = retroRestClient.me()
154157
if (!userResponse.isSuccessful) {
155-
throw APIResponseException("authenticate", url, userResponse)
158+
throw APIResponseException("authenticate", url, userResponse.code(), userResponse.parseErrorBody(moshi))
156159
}
157160

158161
return userResponse.body()!!
@@ -165,7 +168,12 @@ open class CoderRestClient(
165168
suspend fun workspaces(): List<Workspace> {
166169
val workspacesResponse = retroRestClient.workspaces("owner:me")
167170
if (!workspacesResponse.isSuccessful) {
168-
throw APIResponseException("retrieve workspaces", url, workspacesResponse)
171+
throw APIResponseException(
172+
"retrieve workspaces",
173+
url,
174+
workspacesResponse.code(),
175+
workspacesResponse.parseErrorBody(moshi)
176+
)
169177
}
170178

171179
return workspacesResponse.body()!!.workspaces
@@ -178,7 +186,12 @@ open class CoderRestClient(
178186
suspend fun workspace(workspaceID: UUID): Workspace {
179187
val workspacesResponse = retroRestClient.workspace(workspaceID)
180188
if (!workspacesResponse.isSuccessful) {
181-
throw APIResponseException("retrieve workspace", url, workspacesResponse)
189+
throw APIResponseException(
190+
"retrieve workspace",
191+
url,
192+
workspacesResponse.code(),
193+
workspacesResponse.parseErrorBody(moshi)
194+
)
182195
}
183196

184197
return workspacesResponse.body()!!
@@ -209,15 +222,25 @@ open class CoderRestClient(
209222
val resourcesResponse =
210223
retroRestClient.templateVersionResources(workspace.latestBuild.templateVersionID)
211224
if (!resourcesResponse.isSuccessful) {
212-
throw APIResponseException("retrieve resources for ${workspace.name}", url, resourcesResponse)
225+
throw APIResponseException(
226+
"retrieve resources for ${workspace.name}",
227+
url,
228+
resourcesResponse.code(),
229+
resourcesResponse.parseErrorBody(moshi)
230+
)
213231
}
214232
return resourcesResponse.body()!!
215233
}
216234

217235
suspend fun buildInfo(): BuildInfo {
218236
val buildInfoResponse = retroRestClient.buildInfo()
219237
if (!buildInfoResponse.isSuccessful) {
220-
throw APIResponseException("retrieve build information", url, buildInfoResponse)
238+
throw APIResponseException(
239+
"retrieve build information",
240+
url,
241+
buildInfoResponse.code(),
242+
buildInfoResponse.parseErrorBody(moshi)
243+
)
221244
}
222245
return buildInfoResponse.body()!!
223246
}
@@ -228,7 +251,12 @@ open class CoderRestClient(
228251
private suspend fun template(templateID: UUID): Template {
229252
val templateResponse = retroRestClient.template(templateID)
230253
if (!templateResponse.isSuccessful) {
231-
throw APIResponseException("retrieve template with ID $templateID", url, templateResponse)
254+
throw APIResponseException(
255+
"retrieve template with ID $templateID",
256+
url,
257+
templateResponse.code(),
258+
templateResponse.parseErrorBody(moshi)
259+
)
232260
}
233261
return templateResponse.body()!!
234262
}
@@ -240,7 +268,12 @@ open class CoderRestClient(
240268
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.START)
241269
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
242270
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
243-
throw APIResponseException("start workspace ${workspace.name}", url, buildResponse)
271+
throw APIResponseException(
272+
"start workspace ${workspace.name}",
273+
url,
274+
buildResponse.code(),
275+
buildResponse.parseErrorBody(moshi)
276+
)
244277
}
245278
return buildResponse.body()!!
246279
}
@@ -251,7 +284,12 @@ open class CoderRestClient(
251284
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.STOP)
252285
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
253286
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
254-
throw APIResponseException("stop workspace ${workspace.name}", url, buildResponse)
287+
throw APIResponseException(
288+
"stop workspace ${workspace.name}",
289+
url,
290+
buildResponse.code(),
291+
buildResponse.parseErrorBody(moshi)
292+
)
255293
}
256294
return buildResponse.body()!!
257295
}
@@ -263,7 +301,12 @@ open class CoderRestClient(
263301
val buildRequest = CreateWorkspaceBuildRequest(null, WorkspaceTransition.DELETE, false)
264302
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
265303
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
266-
throw APIResponseException("delete workspace ${workspace.name}", url, buildResponse)
304+
throw APIResponseException(
305+
"delete workspace ${workspace.name}",
306+
url,
307+
buildResponse.code(),
308+
buildResponse.parseErrorBody(moshi)
309+
)
267310
}
268311
}
269312

@@ -283,7 +326,12 @@ open class CoderRestClient(
283326
CreateWorkspaceBuildRequest(template.activeVersionID, WorkspaceTransition.START)
284327
val buildResponse = retroRestClient.createWorkspaceBuild(workspace.id, buildRequest)
285328
if (buildResponse.code() != HttpURLConnection.HTTP_CREATED) {
286-
throw APIResponseException("update workspace ${workspace.name}", url, buildResponse)
329+
throw APIResponseException(
330+
"update workspace ${workspace.name}",
331+
url,
332+
buildResponse.code(),
333+
buildResponse.parseErrorBody(moshi)
334+
)
287335
}
288336
return buildResponse.body()!!
289337
}
@@ -296,3 +344,13 @@ open class CoderRestClient(
296344
}
297345
}
298346
}
347+
348+
private fun Response<*>.parseErrorBody(moshi: Moshi): ApiErrorResponse? {
349+
val errorBody = this.errorBody() ?: return null
350+
return try {
351+
val adapter = moshi.adapter(ApiErrorResponse::class.java)
352+
adapter.fromJson(errorBody.string())
353+
} catch (e: Exception) {
354+
null
355+
}
356+
}

0 commit comments

Comments
 (0)