Skip to content

Commit 552e9fe

Browse files
authored
fix: avoid returning 500 on apps when workspace stopped (#11656)
1 parent 1be119b commit 552e9fe

File tree

5 files changed

+61
-2
lines changed

5 files changed

+61
-2
lines changed

coderd/workspaceapps/apptest/apptest.go

+19
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"golang.org/x/xerrors"
2727

2828
"github.com/coder/coder/v2/coderd/coderdtest"
29+
"github.com/coder/coder/v2/coderd/database"
2930
"github.com/coder/coder/v2/coderd/rbac"
3031
"github.com/coder/coder/v2/coderd/workspaceapps"
3132
"github.com/coder/coder/v2/codersdk"
@@ -1484,6 +1485,24 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
14841485
assert.Equal(t, "test-app-owner", stats[0].SlugOrPort)
14851486
assert.Equal(t, 1, stats[0].Requests)
14861487
})
1488+
1489+
t.Run("WorkspaceOffline", func(t *testing.T) {
1490+
t.Parallel()
1491+
1492+
appDetails := setupProxyTest(t, nil)
1493+
1494+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
1495+
defer cancel()
1496+
1497+
_ = coderdtest.MustTransitionWorkspace(t, appDetails.SDKClient, appDetails.Workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
1498+
1499+
u := appDetails.PathAppURL(appDetails.Apps.Owner)
1500+
resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, u.String(), nil)
1501+
require.NoError(t, err)
1502+
_ = resp.Body.Close()
1503+
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
1504+
require.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type"))
1505+
})
14871506
}
14881507

14891508
type fakeStatsReporter struct {

coderd/workspaceapps/db.go

+3
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r *
103103
if xerrors.Is(err, sql.ErrNoRows) {
104104
WriteWorkspaceApp404(p.Logger, p.DashboardURL, rw, r, &appReq, nil, err.Error())
105105
return nil, "", false
106+
} else if xerrors.Is(err, errWorkspaceStopped) {
107+
WriteWorkspaceOffline(p.Logger, p.DashboardURL, rw, r, &appReq)
108+
return nil, "", false
106109
} else if err != nil {
107110
WriteWorkspaceApp500(p.Logger, p.DashboardURL, rw, r, &appReq, err, "get app details from database")
108111
return nil, "", false

coderd/workspaceapps/errors.go

+27
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package workspaceapps
22

33
import (
4+
"fmt"
45
"net/http"
56
"net/url"
67

78
"cdr.dev/slog"
9+
"github.com/coder/coder/v2/codersdk"
810
"github.com/coder/coder/v2/site"
911
)
1012

@@ -90,3 +92,28 @@ func WriteWorkspaceAppOffline(log slog.Logger, accessURL *url.URL, rw http.Respo
9092
DashboardURL: accessURL.String(),
9193
})
9294
}
95+
96+
// WriteWorkspaceOffline writes a HTML 400 error page for a workspace app. If
97+
// appReq is not nil, it will be used to log the request details at debug level.
98+
func WriteWorkspaceOffline(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, appReq *Request) {
99+
if appReq != nil {
100+
slog.Helper()
101+
log.Debug(r.Context(),
102+
"workspace app unavailable: workspace stopped",
103+
slog.F("username_or_id", appReq.UsernameOrID),
104+
slog.F("workspace_and_agent", appReq.WorkspaceAndAgent),
105+
slog.F("workspace_name_or_id", appReq.WorkspaceNameOrID),
106+
slog.F("agent_name_or_id", appReq.AgentNameOrID),
107+
slog.F("app_slug_or_port", appReq.AppSlugOrPort),
108+
slog.F("hostname_prefix", appReq.Prefix),
109+
)
110+
}
111+
112+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
113+
Status: http.StatusBadRequest,
114+
Title: "Workspace Offline",
115+
Description: fmt.Sprintf("Last workspace transition was to the %q state. Start the workspace to access its applications.", codersdk.WorkspaceTransitionStop),
116+
RetryEnabled: false,
117+
DashboardURL: accessURL.String(),
118+
})
119+
}

coderd/workspaceapps/request.go

+10-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"github.com/coder/coder/v2/codersdk"
1818
)
1919

20+
var errWorkspaceStopped = xerrors.New("stopped workspace")
21+
2022
type AccessMethod string
2123

2224
const (
@@ -260,10 +262,17 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR
260262
if err != nil {
261263
return nil, xerrors.Errorf("get workspace agents: %w", err)
262264
}
265+
build, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID)
266+
if err != nil {
267+
return nil, xerrors.Errorf("get latest workspace build: %w", err)
268+
}
269+
if build.Transition == database.WorkspaceTransitionStop {
270+
return nil, errWorkspaceStopped
271+
}
263272
if len(agents) == 0 {
264273
// TODO(@deansheather): return a 404 if there are no agents in the
265274
// workspace, requires a different error type.
266-
return nil, xerrors.New("no agents in workspace")
275+
return nil, xerrors.Errorf("no agents in workspace: %w", sql.ErrNoRows)
267276
}
268277

269278
// Get workspace apps.

site/src/pages/TerminalPage/TerminalPage.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,8 @@ const TerminalPage: FC = () => {
191191
return;
192192
} else if (!workspaceAgent) {
193193
terminal.writeln(
194-
Language.workspaceAgentErrorMessagePrefix + "no agent found with ID",
194+
Language.workspaceAgentErrorMessagePrefix +
195+
"no agent found with ID, is the workspace started?",
195196
);
196197
return;
197198
}

0 commit comments

Comments
 (0)