Files
coder/coderd/workspaceapps/errors.go
T
Rowan Smith f7650296ce chore: switch agent gone response from 502 to 404 (backport #23090) (#23635)
Backport of #23090 to `release/2.31`.

When a user creates a workspace, opens the web terminal, then the
workspace stops but the web terminal remains open, the web terminal will
retry the connection. Coder would issue a HTTP 502 Bad Gateway response
when this occurred because coderd could not connect to the workspace
agent, however this is problematic as any load balancer sitting in front
of Coder sees a 502 and thinks Coder is unhealthy.

This PR changes the response to a HTTP 404 after internal discussion.

Cherry-picked from merge commit
c33812a430.
2026-03-25 16:49:28 -04:00

151 lines
4.8 KiB
Go

package workspaceapps
import (
"fmt"
"net/http"
"net/url"
"path"
"cdr.dev/slog/v3"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/site"
)
// WriteWorkspaceApp404 writes a HTML 404 error page for a workspace app. If
// appReq is not nil, it will be used to log the request details at debug level.
//
// The 'warnings' parameter is sent to the user, 'details' is only shown in the logs.
func WriteWorkspaceApp404(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, appReq *Request, warnings []string, details string) {
if appReq != nil {
slog.Helper()
log.Debug(r.Context(),
"workspace app 404: "+details,
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),
slog.F("warnings", warnings),
)
}
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Status: http.StatusNotFound,
Title: "Application Not Found",
Description: "The application or workspace you are trying to access does not exist or you do not have permission to access it.",
Warnings: warnings,
Actions: []site.Action{
{
URL: accessURL.String(),
Text: "Back to site",
},
},
})
}
// WriteWorkspaceApp500 writes a HTML 500 error page for a workspace app. If
// appReq is not nil, it's fields will be added to the logged error message.
func WriteWorkspaceApp500(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, appReq *Request, err error, msg string) {
ctx := r.Context()
if appReq != nil {
slog.Helper()
ctx = slog.With(ctx,
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_name_or_port", appReq.AppSlugOrPort),
slog.F("hostname_prefix", appReq.Prefix),
)
}
log.Warn(ctx,
"workspace app auth server error: "+msg,
slog.Error(err),
)
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
Status: http.StatusInternalServerError,
Title: "Internal Server Error",
Description: "An internal server error occurred.",
Actions: []site.Action{
{
URL: accessURL.String(),
Text: "Back to site",
},
},
})
}
// WriteWorkspaceAppOffline writes a HTML 404 error page for a workspace app. If
// appReq is not nil, it will be used to log the request details at debug level.
func WriteWorkspaceAppOffline(log slog.Logger, accessURL *url.URL, rw http.ResponseWriter, r *http.Request, appReq *Request, msg string) {
if appReq != nil {
slog.Helper()
log.Debug(r.Context(),
"workspace app unavailable: "+msg,
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.StatusNotFound,
Title: "Application Unavailable",
Description: msg,
Actions: []site.Action{
{
Text: "Retry",
},
{
URL: accessURL.String(),
Text: "Back to site",
},
},
})
}
// 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),
)
}
actions := []site.Action{
{
URL: accessURL.String(),
Text: "Back to site",
},
}
workspaceURL, err := url.Parse(accessURL.String())
if err == nil {
workspaceURL.Path = path.Join(accessURL.Path, "@"+appReq.UsernameOrID, appReq.WorkspaceNameOrID)
actions = append(actions, site.Action{
URL: workspaceURL.String(),
Text: "View workspace",
})
}
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),
Actions: actions,
})
}