Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d165d76

Browse files
authored
feat: static error page in applications handlers (#4299)
1 parent ce95344 commit d165d76

File tree

9 files changed

+172
-220
lines changed

9 files changed

+172
-220
lines changed

coderd/workspaceapps.go

Lines changed: 92 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"context"
66
"crypto/sha256"
7+
"database/sql"
78
"encoding/base64"
89
"encoding/json"
910
"fmt"
@@ -66,10 +67,9 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
6667
Workspace: workspace,
6768
Agent: agent,
6869
// We do not support port proxying for paths.
69-
AppName: chi.URLParam(r, "workspaceapp"),
70-
Port: 0,
71-
Path: chiPath,
72-
DashboardOnError: true,
70+
AppName: chi.URLParam(r, "workspaceapp"),
71+
Port: 0,
72+
Path: chiPath,
7373
}, rw, r)
7474
}
7575

@@ -162,33 +162,31 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht
162162
}
163163

164164
api.proxyWorkspaceApplication(proxyApplication{
165-
Workspace: workspace,
166-
Agent: agent,
167-
AppName: app.AppName,
168-
Port: app.Port,
169-
Path: r.URL.Path,
170-
DashboardOnError: false,
165+
Workspace: workspace,
166+
Agent: agent,
167+
AppName: app.AppName,
168+
Port: app.Port,
169+
Path: r.URL.Path,
171170
}, rw, r)
172171
})).ServeHTTP(rw, r.WithContext(ctx))
173172
})
174173
}
175174
}
176175

177176
func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *http.Request, next http.Handler, host string) (httpapi.ApplicationURL, bool) {
178-
ctx := r.Context()
179-
// Check if the hostname matches the access URL. If it does, the
180-
// user was definitely trying to connect to the dashboard/API.
177+
// Check if the hostname matches the access URL. If it does, the user was
178+
// definitely trying to connect to the dashboard/API.
181179
if httpapi.HostnamesMatch(api.AccessURL.Hostname(), host) {
182180
next.ServeHTTP(rw, r)
183181
return httpapi.ApplicationURL{}, false
184182
}
185183

186-
// Split the subdomain so we can parse the application details and
187-
// verify it matches the configured app hostname later.
184+
// Split the subdomain so we can parse the application details and verify it
185+
// matches the configured app hostname later.
188186
subdomain, rest := httpapi.SplitSubdomain(host)
189187
if rest == "" {
190-
// If there are no periods in the hostname, then it can't be a
191-
// valid application URL.
188+
// If there are no periods in the hostname, then it can't be a valid
189+
// application URL.
192190
next.ServeHTTP(rw, r)
193191
return httpapi.ApplicationURL{}, false
194192
}
@@ -197,27 +195,34 @@ func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *htt
197195
// Parse the application URL from the subdomain.
198196
app, err := httpapi.ParseSubdomainAppURL(subdomain)
199197
if err != nil {
200-
// If it isn't a valid app URL and the base domain doesn't match
201-
// the configured app hostname, this request was probably
202-
// destined for the dashboard/API router.
198+
// If it isn't a valid app URL and the base domain doesn't match the
199+
// configured app hostname, this request was probably destined for the
200+
// dashboard/API router.
203201
if !matchingBaseHostname {
204202
next.ServeHTTP(rw, r)
205203
return httpapi.ApplicationURL{}, false
206204
}
207205

208-
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
209-
Message: "Could not parse subdomain application URL.",
210-
Detail: err.Error(),
206+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
207+
Status: http.StatusBadRequest,
208+
Title: "Invalid application URL",
209+
Description: fmt.Sprintf("Could not parse subdomain application URL %q: %s", subdomain, err.Error()),
210+
RetryEnabled: false,
211+
DashboardURL: api.AccessURL.String(),
211212
})
212213
return httpapi.ApplicationURL{}, false
213214
}
214215

215-
// At this point we've verified that the subdomain looks like a
216-
// valid application URL, so the base hostname should match the
217-
// configured app hostname.
216+
// At this point we've verified that the subdomain looks like a valid
217+
// application URL, so the base hostname should match the configured app
218+
// hostname.
218219
if !matchingBaseHostname {
219-
httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{
220-
Message: "The server does not accept application requests on this hostname.",
220+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
221+
Status: http.StatusNotFound,
222+
Title: "Not Found",
223+
Description: "The server does not accept application requests on this hostname.",
224+
RetryEnabled: false,
225+
DashboardURL: api.AccessURL.String(),
221226
})
222227
return httpapi.ApplicationURL{}, false
223228
}
@@ -230,12 +235,10 @@ func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *htt
230235
// they will be redirected to the route below. If the user does have a session
231236
// key but insufficient permissions a static error page will be rendered.
232237
func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, host string) bool {
233-
ctx := r.Context()
234238
_, ok := httpmw.APIKeyOptional(r)
235239
if ok {
236240
if !api.Authorize(r, rbac.ActionCreate, workspace.ApplicationConnectRBAC()) {
237-
// TODO: This should be a static error page.
238-
httpapi.ResourceNotFound(rw)
241+
renderApplicationNotFound(rw, r, api.AccessURL)
239242
return false
240243
}
241244

@@ -249,9 +252,14 @@ func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.R
249252
// Exchange the encoded API key for a real one.
250253
_, apiKey, err := decryptAPIKey(r.Context(), api.Database, encryptedAPIKey)
251254
if err != nil {
252-
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
253-
Message: "Could not decrypt API key. Please remove the query parameter and try again.",
254-
Detail: err.Error(),
255+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
256+
Status: http.StatusBadRequest,
257+
Title: "Bad Request",
258+
Description: "Could not decrypt API key. Please remove the query parameter and try again.",
259+
// Retry is disabled because the user needs to remove the query
260+
// parameter before they try again.
261+
RetryEnabled: false,
262+
DashboardURL: api.AccessURL.String(),
255263
})
256264
return false
257265
}
@@ -302,6 +310,10 @@ func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.R
302310

303311
// workspaceApplicationAuth is an endpoint on the main router that handles
304312
// redirects from the subdomain handler.
313+
//
314+
// This endpoint is under /api so we don't return the friendly error page here.
315+
// Any errors on this endpoint should be errors that are unlikely to happen
316+
// in production unless the user messes with the URL.
305317
func (api *API) workspaceApplicationAuth(rw http.ResponseWriter, r *http.Request) {
306318
ctx := r.Context()
307319
if api.AppHostname == "" {
@@ -413,11 +425,6 @@ type proxyApplication struct {
413425
Port uint16
414426
// Path must either be empty or have a leading slash.
415427
Path string
416-
417-
// DashboardOnError determines whether or not the dashboard should be
418-
// rendered on error. This should be set for proxy path URLs but not
419-
// hostname based URLs.
420-
DashboardOnError bool
421428
}
422429

423430
func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.ResponseWriter, r *http.Request) {
@@ -439,17 +446,28 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res
439446
AgentID: proxyApp.Agent.ID,
440447
Name: proxyApp.AppName,
441448
})
449+
if xerrors.Is(err, sql.ErrNoRows) {
450+
renderApplicationNotFound(rw, r, api.AccessURL)
451+
return
452+
}
442453
if err != nil {
443-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
444-
Message: "Internal error fetching workspace application.",
445-
Detail: err.Error(),
454+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
455+
Status: http.StatusInternalServerError,
456+
Title: "Internal Server Error",
457+
Description: "Could not fetch workspace application: " + err.Error(),
458+
RetryEnabled: true,
459+
DashboardURL: api.AccessURL.String(),
446460
})
447461
return
448462
}
449463

450464
if !app.Url.Valid {
451-
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
452-
Message: fmt.Sprintf("Application %s does not have a url.", app.Name),
465+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
466+
Status: http.StatusBadRequest,
467+
Title: "Bad Request",
468+
Description: fmt.Sprintf("Application %q does not have a URL set.", app.Name),
469+
RetryEnabled: true,
470+
DashboardURL: api.AccessURL.String(),
453471
})
454472
return
455473
}
@@ -458,9 +476,12 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res
458476

459477
appURL, err := url.Parse(internalURL)
460478
if err != nil {
461-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
462-
Message: fmt.Sprintf("App URL %q is invalid.", internalURL),
463-
Detail: err.Error(),
479+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
480+
Status: http.StatusBadRequest,
481+
Title: "Bad Request",
482+
Description: fmt.Sprintf("Application has an invalid URL %q: %s", internalURL, err.Error()),
483+
RetryEnabled: true,
484+
DashboardURL: api.AccessURL.String(),
464485
})
465486
return
466487
}
@@ -489,28 +510,23 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res
489510

490511
proxy := httputil.NewSingleHostReverseProxy(appURL)
491512
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
492-
if proxyApp.DashboardOnError {
493-
// To pass friendly errors to the frontend, special meta tags are
494-
// overridden in the index.html with the content passed here.
495-
r = r.WithContext(site.WithAPIResponse(ctx, site.APIResponse{
496-
StatusCode: http.StatusBadGateway,
497-
Message: err.Error(),
498-
}))
499-
api.siteHandler.ServeHTTP(w, r)
500-
return
501-
}
502-
503-
httpapi.Write(ctx, w, http.StatusBadGateway, codersdk.Response{
504-
Message: "Failed to proxy request to application.",
505-
Detail: err.Error(),
513+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
514+
Status: http.StatusBadGateway,
515+
Title: "Bad Gateway",
516+
Description: "Failed to proxy request to application: " + err.Error(),
517+
RetryEnabled: true,
518+
DashboardURL: api.AccessURL.String(),
506519
})
507520
}
508521

509522
conn, release, err := api.workspaceAgentCache.Acquire(r, proxyApp.Agent.ID)
510523
if err != nil {
511-
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
512-
Message: "Failed to dial workspace agent.",
513-
Detail: err.Error(),
524+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
525+
Status: http.StatusBadGateway,
526+
Title: "Bad Gateway",
527+
Description: "Could not connect to workspace agent: " + err.Error(),
528+
RetryEnabled: true,
529+
DashboardURL: api.AccessURL.String(),
514530
})
515531
return
516532
}
@@ -648,3 +664,15 @@ func decryptAPIKey(ctx context.Context, db database.Store, encryptedAPIKey strin
648664

649665
return key, payload.APIKey, nil
650666
}
667+
668+
// renderApplicationNotFound should always be used when the app is not found or
669+
// the current user doesn't have permission to access it.
670+
func renderApplicationNotFound(rw http.ResponseWriter, r *http.Request, accessURL *url.URL) {
671+
site.RenderStaticErrorPage(rw, r, site.ErrorPageData{
672+
Status: http.StatusNotFound,
673+
Title: "Application not found",
674+
Description: "The application or workspace you are trying to access does not exist.",
675+
RetryEnabled: false,
676+
DashboardURL: accessURL.String(),
677+
})
678+
}

coderd/workspaceapps_test.go

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
258258
resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/fake/", nil)
259259
require.NoError(t, err)
260260
defer resp.Body.Close()
261-
// this is 200 OK because it returns a dashboard page
262-
require.Equal(t, http.StatusOK, resp.StatusCode)
261+
require.Equal(t, http.StatusBadGateway, resp.StatusCode)
263262
})
264263
}
265264

@@ -529,10 +528,9 @@ func TestWorkspaceAppsProxySubdomainBlocked(t *testing.T) {
529528

530529
// Should have an error response.
531530
require.Equal(t, http.StatusNotFound, resp.StatusCode)
532-
var resBody codersdk.Response
533-
err = json.NewDecoder(resp.Body).Decode(&resBody)
531+
body, err := io.ReadAll(resp.Body)
534532
require.NoError(t, err)
535-
require.Contains(t, resBody.Message, "does not accept application requests on this hostname")
533+
require.Contains(t, string(body), "does not accept application requests on this hostname")
536534
})
537535

538536
t.Run("InvalidSubdomain", func(t *testing.T) {
@@ -547,12 +545,11 @@ func TestWorkspaceAppsProxySubdomainBlocked(t *testing.T) {
547545
require.NoError(t, err)
548546
defer resp.Body.Close()
549547

550-
// Should have an error response.
548+
// Should have a HTML error response.
551549
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
552-
var resBody codersdk.Response
553-
err = json.NewDecoder(resp.Body).Decode(&resBody)
550+
body, err := io.ReadAll(resp.Body)
554551
require.NoError(t, err)
555-
require.Contains(t, resBody.Message, "Could not parse subdomain application URL")
552+
require.Contains(t, string(body), "Could not parse subdomain application URL")
556553
})
557554
}
558555

site/index.html

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,6 @@
1616
<meta property="og:type" content="website" />
1717
<meta property="csp-nonce" content="{{ .CSP.Nonce }}" />
1818
<meta property="csrf-token" content="{{ .CSRF.Token }}" />
19-
<meta
20-
id="api-response"
21-
data-statuscode="{{ .APIResponse.StatusCode }}"
22-
data-message="{{ .APIResponse.Message }}"
23-
/>
2419
<!-- We need to set data-react-helmet to be able to override it in the workspace page -->
2520
<link
2621
rel="alternate icon"

0 commit comments

Comments
 (0)