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

Lines changed: 8 additions & 3 deletions
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

Lines changed: 18 additions & 10 deletions
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

Lines changed: 69 additions & 11 deletions
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy