diff --git a/coderd/workspaceapps/apptest/apptest.go b/coderd/workspaceapps/apptest/apptest.go index a8c593c1ec89d..a38189a1ff25c 100644 --- a/coderd/workspaceapps/apptest/apptest.go +++ b/coderd/workspaceapps/apptest/apptest.go @@ -26,6 +26,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/coderdtest" + "github.com/coder/coder/v2/coderd/database" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/codersdk" @@ -1484,6 +1485,24 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) { assert.Equal(t, "test-app-owner", stats[0].SlugOrPort) assert.Equal(t, 1, stats[0].Requests) }) + + t.Run("WorkspaceOffline", func(t *testing.T) { + t.Parallel() + + appDetails := setupProxyTest(t, nil) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _ = coderdtest.MustTransitionWorkspace(t, appDetails.SDKClient, appDetails.Workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) + + u := appDetails.PathAppURL(appDetails.Apps.Owner) + resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, u.String(), nil) + require.NoError(t, err) + _ = resp.Body.Close() + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + require.Equal(t, "text/html; charset=utf-8", resp.Header.Get("Content-Type")) + }) } type fakeStatsReporter struct { diff --git a/coderd/workspaceapps/db.go b/coderd/workspaceapps/db.go index 9b196a4b7480e..b17c4a4a05c69 100644 --- a/coderd/workspaceapps/db.go +++ b/coderd/workspaceapps/db.go @@ -103,6 +103,9 @@ func (p *DBTokenProvider) Issue(ctx context.Context, rw http.ResponseWriter, r * if xerrors.Is(err, sql.ErrNoRows) { WriteWorkspaceApp404(p.Logger, p.DashboardURL, rw, r, &appReq, nil, err.Error()) return nil, "", false + } else if xerrors.Is(err, errWorkspaceStopped) { + WriteWorkspaceOffline(p.Logger, p.DashboardURL, rw, r, &appReq) + return nil, "", false } else if err != nil { WriteWorkspaceApp500(p.Logger, p.DashboardURL, rw, r, &appReq, err, "get app details from database") return nil, "", false diff --git a/coderd/workspaceapps/errors.go b/coderd/workspaceapps/errors.go index bcc890c81e89a..64d61de3678ed 100644 --- a/coderd/workspaceapps/errors.go +++ b/coderd/workspaceapps/errors.go @@ -1,10 +1,12 @@ package workspaceapps import ( + "fmt" "net/http" "net/url" "cdr.dev/slog" + "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/site" ) @@ -90,3 +92,28 @@ func WriteWorkspaceAppOffline(log slog.Logger, accessURL *url.URL, rw http.Respo DashboardURL: accessURL.String(), }) } + +// WriteWorkspaceOffline writes a HTML 400 error page for a workspace app. If +// appReq is not nil, it will be used to log the request details at debug level. +func WriteWorkspaceOffline(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, appReq *Request) { + if appReq != nil { + slog.Helper() + log.Debug(r.Context(), + "workspace app unavailable: workspace stopped", + slog.F("username_or_id", appReq.UsernameOrID), + slog.F("workspace_and_agent", appReq.WorkspaceAndAgent), + slog.F("workspace_name_or_id", appReq.WorkspaceNameOrID), + slog.F("agent_name_or_id", appReq.AgentNameOrID), + slog.F("app_slug_or_port", appReq.AppSlugOrPort), + slog.F("hostname_prefix", appReq.Prefix), + ) + } + + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusBadRequest, + Title: "Workspace Offline", + Description: fmt.Sprintf("Last workspace transition was to the %q state. Start the workspace to access its applications.", codersdk.WorkspaceTransitionStop), + RetryEnabled: false, + DashboardURL: accessURL.String(), + }) +} diff --git a/coderd/workspaceapps/request.go b/coderd/workspaceapps/request.go index c46413d22961f..834ed6f449b2f 100644 --- a/coderd/workspaceapps/request.go +++ b/coderd/workspaceapps/request.go @@ -17,6 +17,8 @@ import ( "github.com/coder/coder/v2/codersdk" ) +var errWorkspaceStopped = xerrors.New("stopped workspace") + type AccessMethod string const ( @@ -260,10 +262,17 @@ func (r Request) getDatabase(ctx context.Context, db database.Store) (*databaseR if err != nil { return nil, xerrors.Errorf("get workspace agents: %w", err) } + build, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspace.ID) + if err != nil { + return nil, xerrors.Errorf("get latest workspace build: %w", err) + } + if build.Transition == database.WorkspaceTransitionStop { + return nil, errWorkspaceStopped + } if len(agents) == 0 { // TODO(@deansheather): return a 404 if there are no agents in the // workspace, requires a different error type. - return nil, xerrors.New("no agents in workspace") + return nil, xerrors.Errorf("no agents in workspace: %w", sql.ErrNoRows) } // Get workspace apps. diff --git a/site/src/pages/TerminalPage/TerminalPage.tsx b/site/src/pages/TerminalPage/TerminalPage.tsx index e86d874006a94..8e6dbcdbcdb05 100644 --- a/site/src/pages/TerminalPage/TerminalPage.tsx +++ b/site/src/pages/TerminalPage/TerminalPage.tsx @@ -191,7 +191,8 @@ const TerminalPage: FC = () => { return; } else if (!workspaceAgent) { terminal.writeln( - Language.workspaceAgentErrorMessagePrefix + "no agent found with ID", + Language.workspaceAgentErrorMessagePrefix + + "no agent found with ID, is the workspace started?", ); return; } 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