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

Lines changed: 19 additions & 0 deletions
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

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

Lines changed: 27 additions & 0 deletions
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

Lines changed: 10 additions & 1 deletion
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

Lines changed: 2 additions & 1 deletion
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)
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