Skip to content

Commit 3a0e8af

Browse files
authored
feat: add view workspace button to app error page (#20960)
Closes #19984 As part of this, I refactored the error template to take in a slice of actions rather than using individual booleans and strings to control the behavior. We decided a link resolves the issue for now so that is what I added, although we may want to consider a way to start the workspace and follow the logs dynamically on that page and then show the app when finished (similar to the tasks page), or at least make the link automatically start the workspace instead of only taking you to the dashboard where you have to then start the workspace.
1 parent 50d42ab commit 3a0e8af

File tree

9 files changed

+233
-107
lines changed

9 files changed

+233
-107
lines changed

coderd/idpsync/idpsync.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -251,13 +251,16 @@ type HTTPError struct {
251251
func (e HTTPError) Write(rw http.ResponseWriter, r *http.Request) {
252252
if e.RenderStaticPage {
253253
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
254-
Status: e.Code,
255-
HideStatus: true,
256-
Title: e.Msg,
257-
Description: e.Detail,
258-
RetryEnabled: false,
259-
DashboardURL: "/login",
260-
254+
Status: e.Code,
255+
HideStatus: true,
256+
Title: e.Msg,
257+
Description: e.Detail,
258+
Actions: []site.Action{
259+
{
260+
URL: "/login",
261+
Text: "Back to site",
262+
},
263+
},
261264
RenderDescriptionMarkdown: e.RenderDetailMarkdown,
262265
})
263266
return

coderd/oauth2provider/authorize.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,18 @@ func ShowAuthorizePage(accessURL *url.URL) http.HandlerFunc {
7575

7676
callbackURL, err := url.Parse(app.CallbackURL)
7777
if err != nil {
78-
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{Status: http.StatusInternalServerError, HideStatus: false, Title: "Internal Server Error", Description: err.Error(), RetryEnabled: false, DashboardURL: accessURL.String(), Warnings: nil})
78+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
79+
Status: http.StatusInternalServerError,
80+
HideStatus: false,
81+
Title: "Internal Server Error",
82+
Description: err.Error(),
83+
Actions: []site.Action{
84+
{
85+
URL: accessURL.String(),
86+
Text: "Back to site",
87+
},
88+
},
89+
})
7990
return
8091
}
8192

@@ -85,7 +96,19 @@ func ShowAuthorizePage(accessURL *url.URL) http.HandlerFunc {
8596
for i, err := range validationErrs {
8697
errStr[i] = err.Detail
8798
}
88-
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{Status: http.StatusBadRequest, HideStatus: false, Title: "Invalid Query Parameters", Description: "One or more query parameters are missing or invalid.", RetryEnabled: false, DashboardURL: accessURL.String(), Warnings: errStr})
99+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
100+
Status: http.StatusBadRequest,
101+
HideStatus: false,
102+
Title: "Invalid Query Parameters",
103+
Description: "One or more query parameters are missing or invalid.",
104+
Warnings: errStr,
105+
Actions: []site.Action{
106+
{
107+
URL: accessURL.String(),
108+
Text: "Back to site",
109+
},
110+
},
111+
})
89112
return
90113
}
91114

coderd/tailnet.go

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -199,10 +199,9 @@ func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID u
199199
proxy := httputil.NewSingleHostReverseProxy(&tgt)
200200
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, theErr error) {
201201
var (
202-
desc = "Failed to proxy request to application: " + theErr.Error()
203-
additionalInfo = ""
204-
additionalButtonLink = ""
205-
additionalButtonText = ""
202+
desc = "Failed to proxy request to application: " + theErr.Error()
203+
additionalInfo = ""
204+
actions = []site.Action{}
206205
)
207206

208207
var tlsError tls.RecordHeaderError
@@ -222,21 +221,28 @@ func (s *ServerTailnet) ReverseProxy(targetURL, dashboardURL *url.URL, agentID u
222221
app = app.ChangePortProtocol(targetProtocol)
223222

224223
switchURL.Host = fmt.Sprintf("%s%s", app.String(), strings.TrimPrefix(wildcardHostname, "*"))
225-
additionalButtonLink = switchURL.String()
226-
additionalButtonText = fmt.Sprintf("Switch to %s", strings.ToUpper(targetProtocol))
224+
actions = append(actions, site.Action{
225+
URL: switchURL.String(),
226+
Text: fmt.Sprintf("Switch to %s", strings.ToUpper(targetProtocol)),
227+
})
227228
additionalInfo += fmt.Sprintf("This error seems to be due to an app protocol mismatch, try switching to %s.", strings.ToUpper(targetProtocol))
228229
}
229230
}
230231

231232
site.RenderStaticErrorPage(w, r, site.ErrorPageData{
232-
Status: http.StatusBadGateway,
233-
Title: "Bad Gateway",
234-
Description: desc,
235-
RetryEnabled: true,
236-
DashboardURL: dashboardURL.String(),
237-
AdditionalInfo: additionalInfo,
238-
AdditionalButtonLink: additionalButtonLink,
239-
AdditionalButtonText: additionalButtonText,
233+
Status: http.StatusBadGateway,
234+
Title: "Bad Gateway",
235+
Description: desc,
236+
Actions: append(actions, []site.Action{
237+
{
238+
Text: "Retry",
239+
},
240+
{
241+
URL: dashboardURL.String(),
242+
Text: "Back to site",
243+
},
244+
}...),
245+
AdditionalInfo: additionalInfo,
240246
})
241247
}
242248
proxy.Director = s.director(agentID, proxy.Director)

coderd/workspaceapps/errors.go

Lines changed: 52 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"net/http"
66
"net/url"
7+
"path"
78

89
"cdr.dev/slog"
910
"github.com/coder/coder/v2/codersdk"
@@ -30,12 +31,16 @@ func WriteWorkspaceApp404(log slog.Logger, accessURL *url.URL, rw http.ResponseW
3031
}
3132

3233
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
33-
Status: http.StatusNotFound,
34-
Title: "Application Not Found",
35-
Description: "The application or workspace you are trying to access does not exist or you do not have permission to access it.",
36-
RetryEnabled: false,
37-
DashboardURL: accessURL.String(),
38-
Warnings: warnings,
34+
Status: http.StatusNotFound,
35+
Title: "Application Not Found",
36+
Description: "The application or workspace you are trying to access does not exist or you do not have permission to access it.",
37+
Warnings: warnings,
38+
Actions: []site.Action{
39+
{
40+
URL: accessURL.String(),
41+
Text: "Back to site",
42+
},
43+
},
3944
})
4045
}
4146

@@ -60,11 +65,15 @@ func WriteWorkspaceApp500(log slog.Logger, accessURL *url.URL, rw http.ResponseW
6065
)
6166

6267
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
63-
Status: http.StatusInternalServerError,
64-
Title: "Internal Server Error",
65-
Description: "An internal server error occurred.",
66-
RetryEnabled: false,
67-
DashboardURL: accessURL.String(),
68+
Status: http.StatusInternalServerError,
69+
Title: "Internal Server Error",
70+
Description: "An internal server error occurred.",
71+
Actions: []site.Action{
72+
{
73+
URL: accessURL.String(),
74+
Text: "Back to site",
75+
},
76+
},
6877
})
6978
}
7079

@@ -85,11 +94,18 @@ func WriteWorkspaceAppOffline(log slog.Logger, accessURL *url.URL, rw http.Respo
8594
}
8695

8796
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
88-
Status: http.StatusBadGateway,
89-
Title: "Application Unavailable",
90-
Description: msg,
91-
RetryEnabled: true,
92-
DashboardURL: accessURL.String(),
97+
Status: http.StatusBadGateway,
98+
Title: "Application Unavailable",
99+
Description: msg,
100+
Actions: []site.Action{
101+
{
102+
Text: "Retry",
103+
},
104+
{
105+
URL: accessURL.String(),
106+
Text: "Back to site",
107+
},
108+
},
93109
})
94110
}
95111

@@ -109,11 +125,26 @@ func WriteWorkspaceOffline(log slog.Logger, accessURL *url.URL, rw http.Response
109125
)
110126
}
111127

128+
actions := []site.Action{
129+
{
130+
URL: accessURL.String(),
131+
Text: "Back to site",
132+
},
133+
}
134+
135+
workspaceURL, err := url.Parse(accessURL.String())
136+
if err == nil {
137+
workspaceURL.Path = path.Join(accessURL.Path, "@"+appReq.UsernameOrID, appReq.WorkspaceNameOrID)
138+
actions = append(actions, site.Action{
139+
URL: workspaceURL.String(),
140+
Text: "View workspace",
141+
})
142+
}
143+
112144
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(),
145+
Status: http.StatusBadRequest,
146+
Title: "Workspace Offline",
147+
Description: fmt.Sprintf("Last workspace transition was to the %q state. Start the workspace to access its applications.", codersdk.WorkspaceTransitionStop),
148+
Actions: actions,
118149
})
119150
}

coderd/workspaceapps/proxy.go

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -185,10 +185,14 @@ func (s *Server) handleAPIKeySmuggling(rw http.ResponseWriter, r *http.Request,
185185
Status: http.StatusBadRequest,
186186
Title: "Bad Request",
187187
Description: "Could not decrypt API key. Workspace app API key smuggling is not permitted on the primary access URL. Please remove the query parameter and try again.",
188-
// Retry is disabled because the user needs to remove the query
188+
// No retry is included because the user needs to remove the query
189189
// parameter before they try again.
190-
RetryEnabled: false,
191-
DashboardURL: s.DashboardURL.String(),
190+
Actions: []site.Action{
191+
{
192+
URL: s.DashboardURL.String(),
193+
Text: "Back to site",
194+
},
195+
},
192196
})
193197
return false
194198
}
@@ -204,10 +208,14 @@ func (s *Server) handleAPIKeySmuggling(rw http.ResponseWriter, r *http.Request,
204208
Status: http.StatusBadRequest,
205209
Title: "Bad Request",
206210
Description: "Could not decrypt API key. Please remove the query parameter and try again.",
207-
// Retry is disabled because the user needs to remove the query
211+
// No retry is included because the user needs to remove the query
208212
// parameter before they try again.
209-
RetryEnabled: false,
210-
DashboardURL: s.DashboardURL.String(),
213+
Actions: []site.Action{
214+
{
215+
URL: s.DashboardURL.String(),
216+
Text: "Back to site",
217+
},
218+
},
211219
})
212220
return false
213221
}
@@ -224,11 +232,15 @@ func (s *Server) handleAPIKeySmuggling(rw http.ResponseWriter, r *http.Request,
224232
// startup, but we'll check anyways.
225233
s.Logger.Error(r.Context(), "could not split invalid app hostname", slog.F("hostname", s.Hostname))
226234
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
227-
Status: http.StatusInternalServerError,
228-
Title: "Internal Server Error",
229-
Description: "The app is configured with an invalid app wildcard hostname. Please contact an administrator.",
230-
RetryEnabled: false,
231-
DashboardURL: s.DashboardURL.String(),
235+
Status: http.StatusInternalServerError,
236+
Title: "Internal Server Error",
237+
Description: "The app is configured with an invalid app wildcard hostname. Please contact an administrator.",
238+
Actions: []site.Action{
239+
{
240+
URL: s.DashboardURL.String(),
241+
Text: "Back to site",
242+
},
243+
},
232244
})
233245
return false
234246
}
@@ -274,11 +286,15 @@ func (s *Server) handleAPIKeySmuggling(rw http.ResponseWriter, r *http.Request,
274286
func (s *Server) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) {
275287
if s.DisablePathApps {
276288
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
277-
Status: http.StatusForbidden,
278-
Title: "Forbidden",
279-
Description: "Path-based applications are disabled on this Coder deployment by the administrator.",
280-
RetryEnabled: false,
281-
DashboardURL: s.DashboardURL.String(),
289+
Status: http.StatusForbidden,
290+
Title: "Forbidden",
291+
Description: "Path-based applications are disabled on this Coder deployment by the administrator.",
292+
Actions: []site.Action{
293+
{
294+
URL: s.DashboardURL.String(),
295+
Text: "Back to site",
296+
},
297+
},
282298
})
283299
return
284300
}
@@ -287,11 +303,15 @@ func (s *Server) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
287303
// lookup the username from token. We used to redirect by doing this lookup.
288304
if chi.URLParam(r, "user") == codersdk.Me {
289305
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
290-
Status: http.StatusNotFound,
291-
Title: "Application Not Found",
292-
Description: "Applications must be accessed with the full username, not @me.",
293-
RetryEnabled: false,
294-
DashboardURL: s.DashboardURL.String(),
306+
Status: http.StatusNotFound,
307+
Title: "Application Not Found",
308+
Description: "Applications must be accessed with the full username, not @me.",
309+
Actions: []site.Action{
310+
{
311+
URL: s.DashboardURL.String(),
312+
Text: "Back to site",
313+
},
314+
},
295315
})
296316
return
297317
}
@@ -519,11 +539,15 @@ func (s *Server) parseHostname(rw http.ResponseWriter, r *http.Request, next htt
519539
app, err := appurl.ParseSubdomainAppURL(subdomain)
520540
if err != nil {
521541
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
522-
Status: http.StatusBadRequest,
523-
Title: "Invalid Application URL",
524-
Description: fmt.Sprintf("Could not parse subdomain application URL %q: %s", subdomain, err.Error()),
525-
RetryEnabled: false,
526-
DashboardURL: s.DashboardURL.String(),
542+
Status: http.StatusBadRequest,
543+
Title: "Invalid Application URL",
544+
Description: fmt.Sprintf("Could not parse subdomain application URL %q: %s", subdomain, err.Error()),
545+
Actions: []site.Action{
546+
{
547+
URL: s.DashboardURL.String(),
548+
Text: "Back to site",
549+
},
550+
},
527551
})
528552
return appurl.ApplicationURL{}, false
529553
}
@@ -547,11 +571,18 @@ func (s *Server) proxyWorkspaceApp(rw http.ResponseWriter, r *http.Request, appT
547571
appURL, err := url.Parse(appToken.AppURL)
548572
if err != nil {
549573
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
550-
Status: http.StatusBadRequest,
551-
Title: "Bad Request",
552-
Description: fmt.Sprintf("Application has an invalid URL %q: %s", appToken.AppURL, err.Error()),
553-
RetryEnabled: true,
554-
DashboardURL: s.DashboardURL.String(),
574+
Status: http.StatusBadRequest,
575+
Title: "Bad Request",
576+
Description: fmt.Sprintf("Application has an invalid URL %q: %s", appToken.AppURL, err.Error()),
577+
Actions: []site.Action{
578+
{
579+
Text: "Retry",
580+
},
581+
{
582+
URL: s.DashboardURL.String(),
583+
Text: "Back to site",
584+
},
585+
},
555586
})
556587
return
557588
}

enterprise/wsproxy/wsproxy.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -379,8 +379,12 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
379379
HideStatus: true,
380380
Description: "This workspace proxy is DERP-only and cannot be used for browser connections. " +
381381
"Please use a different region directly from the dashboard. Click to be redirected!",
382-
RetryEnabled: false,
383-
DashboardURL: opts.DashboardURL.String(),
382+
Actions: []site.Action{
383+
{
384+
URL: opts.DashboardURL.String(),
385+
Text: "Back to site",
386+
},
387+
},
384388
})
385389
}
386390
serveDerpOnlyHandler := func(r chi.Router) {
@@ -422,8 +426,12 @@ func New(ctx context.Context, opts *Options) (*Server, error) {
422426
HideStatus: true,
423427
Description: "Workspace Proxies route traffic in terminals and apps directly to your workspace. " +
424428
"This page must be loaded from the dashboard. Click to be redirected!",
425-
RetryEnabled: false,
426-
DashboardURL: opts.DashboardURL.String(),
429+
Actions: []site.Action{
430+
{
431+
URL: opts.DashboardURL.String(),
432+
Text: "Back to site",
433+
},
434+
},
427435
})
428436
})
429437

0 commit comments

Comments
 (0)