Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commitd165d76

Browse files
authored
feat: static error page in applications handlers (#4299)
1 parentce95344 commitd165d76

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,hoststring) (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
ifhttpapi.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 itmatches 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
ifrest=="" {
190-
// If there are no periods in the hostname, then it can't be a
191-
//validapplication 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
iferr!=nil {
200-
// If it isn't a valid app URL and the base domain doesn't match
201-
//theconfigured app hostname, this request was probably
202-
//destined for thedashboard/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-
//validapplication URL, so the base hostname should match the
217-
//configured apphostname.
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,hoststring)bool {
233-
ctx:=r.Context()
234238
_,ok:=httpmw.APIKeyOptional(r)
235239
ifok {
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
returnfalse
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
iferr!=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
returnfalse
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
ifapi.AppHostname=="" {
@@ -413,11 +425,6 @@ type proxyApplication struct {
413425
Portuint16
414426
// Path must either be empty or have a leading slash.
415427
Pathstring
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-
DashboardOnErrorbool
421428
}
422429

423430
func (api*API)proxyWorkspaceApplication(proxyAppproxyApplication,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+
ifxerrors.Is(err,sql.ErrNoRows) {
450+
renderApplicationNotFound(rw,r,api.AccessURL)
451+
return
452+
}
442453
iferr!=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
iferr!=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,errerror) {
492-
ifproxyApp.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
iferr!=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
returnkey,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+
funcrenderApplicationNotFound(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
deferresp.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-
varresBody 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
deferresp.Body.Close()
549547

550-
// Should havean error response.
548+
// Should havea HTML error response.
551549
require.Equal(t,http.StatusBadRequest,resp.StatusCode)
552-
varresBody 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
<metaproperty="og:type"content="website"/>
1717
<metaproperty="csp-nonce"content="{{ .CSP.Nonce }}"/>
1818
<metaproperty="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)

[8]ページ先頭

©2009-2025 Movatter.jp