diff --git a/coderd/workspaceapps.go b/coderd/workspaceapps.go index 9799966a6b562..4b0b6eb94c5d5 100644 --- a/coderd/workspaceapps.go +++ b/coderd/workspaceapps.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto/sha256" + "database/sql" "encoding/base64" "encoding/json" "fmt" @@ -66,10 +67,9 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) Workspace: workspace, Agent: agent, // We do not support port proxying for paths. - AppName: chi.URLParam(r, "workspaceapp"), - Port: 0, - Path: chiPath, - DashboardOnError: true, + AppName: chi.URLParam(r, "workspaceapp"), + Port: 0, + Path: chiPath, }, rw, r) } @@ -162,12 +162,11 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht } api.proxyWorkspaceApplication(proxyApplication{ - Workspace: workspace, - Agent: agent, - AppName: app.AppName, - Port: app.Port, - Path: r.URL.Path, - DashboardOnError: false, + Workspace: workspace, + Agent: agent, + AppName: app.AppName, + Port: app.Port, + Path: r.URL.Path, }, rw, r) })).ServeHTTP(rw, r.WithContext(ctx)) }) @@ -175,20 +174,19 @@ func (api *API) handleSubdomainApplications(middlewares ...func(http.Handler) ht } func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *http.Request, next http.Handler, host string) (httpapi.ApplicationURL, bool) { - ctx := r.Context() - // Check if the hostname matches the access URL. If it does, the - // user was definitely trying to connect to the dashboard/API. + // Check if the hostname matches the access URL. If it does, the user was + // definitely trying to connect to the dashboard/API. if httpapi.HostnamesMatch(api.AccessURL.Hostname(), host) { next.ServeHTTP(rw, r) return httpapi.ApplicationURL{}, false } - // Split the subdomain so we can parse the application details and - // verify it matches the configured app hostname later. + // Split the subdomain so we can parse the application details and verify it + // matches the configured app hostname later. subdomain, rest := httpapi.SplitSubdomain(host) if rest == "" { - // If there are no periods in the hostname, then it can't be a - // valid application URL. + // If there are no periods in the hostname, then it can't be a valid + // application URL. next.ServeHTTP(rw, r) return httpapi.ApplicationURL{}, false } @@ -197,27 +195,34 @@ func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *htt // Parse the application URL from the subdomain. app, err := httpapi.ParseSubdomainAppURL(subdomain) if err != nil { - // If it isn't a valid app URL and the base domain doesn't match - // the configured app hostname, this request was probably - // destined for the dashboard/API router. + // If it isn't a valid app URL and the base domain doesn't match the + // configured app hostname, this request was probably destined for the + // dashboard/API router. if !matchingBaseHostname { next.ServeHTTP(rw, r) return httpapi.ApplicationURL{}, false } - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Could not parse subdomain application URL.", - Detail: err.Error(), + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusBadRequest, + Title: "Invalid application URL", + Description: fmt.Sprintf("Could not parse subdomain application URL %q: %s", subdomain, err.Error()), + RetryEnabled: false, + DashboardURL: api.AccessURL.String(), }) return httpapi.ApplicationURL{}, false } - // At this point we've verified that the subdomain looks like a - // valid application URL, so the base hostname should match the - // configured app hostname. + // At this point we've verified that the subdomain looks like a valid + // application URL, so the base hostname should match the configured app + // hostname. if !matchingBaseHostname { - httpapi.Write(ctx, rw, http.StatusNotFound, codersdk.Response{ - Message: "The server does not accept application requests on this hostname.", + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusNotFound, + Title: "Not Found", + Description: "The server does not accept application requests on this hostname.", + RetryEnabled: false, + DashboardURL: api.AccessURL.String(), }) return httpapi.ApplicationURL{}, false } @@ -230,12 +235,10 @@ func (api *API) parseWorkspaceApplicationHostname(rw http.ResponseWriter, r *htt // they will be redirected to the route below. If the user does have a session // key but insufficient permissions a static error page will be rendered. func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.Request, workspace database.Workspace, host string) bool { - ctx := r.Context() _, ok := httpmw.APIKeyOptional(r) if ok { if !api.Authorize(r, rbac.ActionCreate, workspace.ApplicationConnectRBAC()) { - // TODO: This should be a static error page. - httpapi.ResourceNotFound(rw) + renderApplicationNotFound(rw, r, api.AccessURL) return false } @@ -249,9 +252,14 @@ func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.R // Exchange the encoded API key for a real one. _, apiKey, err := decryptAPIKey(r.Context(), api.Database, encryptedAPIKey) if err != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: "Could not decrypt API key. Please remove the query parameter and try again.", - Detail: err.Error(), + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusBadRequest, + Title: "Bad Request", + Description: "Could not decrypt API key. Please remove the query parameter and try again.", + // Retry is disabled because the user needs to remove the query + // parameter before they try again. + RetryEnabled: false, + DashboardURL: api.AccessURL.String(), }) return false } @@ -302,6 +310,10 @@ func (api *API) verifyWorkspaceApplicationAuth(rw http.ResponseWriter, r *http.R // workspaceApplicationAuth is an endpoint on the main router that handles // redirects from the subdomain handler. +// +// This endpoint is under /api so we don't return the friendly error page here. +// Any errors on this endpoint should be errors that are unlikely to happen +// in production unless the user messes with the URL. func (api *API) workspaceApplicationAuth(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() if api.AppHostname == "" { @@ -413,11 +425,6 @@ type proxyApplication struct { Port uint16 // Path must either be empty or have a leading slash. Path string - - // DashboardOnError determines whether or not the dashboard should be - // rendered on error. This should be set for proxy path URLs but not - // hostname based URLs. - DashboardOnError bool } 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 AgentID: proxyApp.Agent.ID, Name: proxyApp.AppName, }) + if xerrors.Is(err, sql.ErrNoRows) { + renderApplicationNotFound(rw, r, api.AccessURL) + return + } if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Internal error fetching workspace application.", - Detail: err.Error(), + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusInternalServerError, + Title: "Internal Server Error", + Description: "Could not fetch workspace application: " + err.Error(), + RetryEnabled: true, + DashboardURL: api.AccessURL.String(), }) return } if !app.Url.Valid { - httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ - Message: fmt.Sprintf("Application %s does not have a url.", app.Name), + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusBadRequest, + Title: "Bad Request", + Description: fmt.Sprintf("Application %q does not have a URL set.", app.Name), + RetryEnabled: true, + DashboardURL: api.AccessURL.String(), }) return } @@ -458,9 +476,12 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res appURL, err := url.Parse(internalURL) if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: fmt.Sprintf("App URL %q is invalid.", internalURL), - Detail: err.Error(), + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusBadRequest, + Title: "Bad Request", + Description: fmt.Sprintf("Application has an invalid URL %q: %s", internalURL, err.Error()), + RetryEnabled: true, + DashboardURL: api.AccessURL.String(), }) return } @@ -489,28 +510,23 @@ func (api *API) proxyWorkspaceApplication(proxyApp proxyApplication, rw http.Res proxy := httputil.NewSingleHostReverseProxy(appURL) proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) { - if proxyApp.DashboardOnError { - // To pass friendly errors to the frontend, special meta tags are - // overridden in the index.html with the content passed here. - r = r.WithContext(site.WithAPIResponse(ctx, site.APIResponse{ - StatusCode: http.StatusBadGateway, - Message: err.Error(), - })) - api.siteHandler.ServeHTTP(w, r) - return - } - - httpapi.Write(ctx, w, http.StatusBadGateway, codersdk.Response{ - Message: "Failed to proxy request to application.", - Detail: err.Error(), + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusBadGateway, + Title: "Bad Gateway", + Description: "Failed to proxy request to application: " + err.Error(), + RetryEnabled: true, + DashboardURL: api.AccessURL.String(), }) } conn, release, err := api.workspaceAgentCache.Acquire(r, proxyApp.Agent.ID) if err != nil { - httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{ - Message: "Failed to dial workspace agent.", - Detail: err.Error(), + site.RenderStaticErrorPage(rw, r, site.ErrorPageData{ + Status: http.StatusBadGateway, + Title: "Bad Gateway", + Description: "Could not connect to workspace agent: " + err.Error(), + RetryEnabled: true, + DashboardURL: api.AccessURL.String(), }) return } @@ -648,3 +664,15 @@ func decryptAPIKey(ctx context.Context, db database.Store, encryptedAPIKey strin return key, payload.APIKey, nil } + +// renderApplicationNotFound should always be used when the app is not found or +// the current user doesn't have permission to access it. +func renderApplicationNotFound(rw http.ResponseWriter, r *http.Request, accessURL *url.URL) { + 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.", + RetryEnabled: false, + DashboardURL: accessURL.String(), + }) +} diff --git a/coderd/workspaceapps_test.go b/coderd/workspaceapps_test.go index e68061b185ec9..b3999b388f1c9 100644 --- a/coderd/workspaceapps_test.go +++ b/coderd/workspaceapps_test.go @@ -258,8 +258,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) { resp, err := client.Request(ctx, http.MethodGet, "/@me/"+workspace.Name+"/apps/fake/", nil) require.NoError(t, err) defer resp.Body.Close() - // this is 200 OK because it returns a dashboard page - require.Equal(t, http.StatusOK, resp.StatusCode) + require.Equal(t, http.StatusBadGateway, resp.StatusCode) }) } @@ -529,10 +528,9 @@ func TestWorkspaceAppsProxySubdomainBlocked(t *testing.T) { // Should have an error response. require.Equal(t, http.StatusNotFound, resp.StatusCode) - var resBody codersdk.Response - err = json.NewDecoder(resp.Body).Decode(&resBody) + body, err := io.ReadAll(resp.Body) require.NoError(t, err) - require.Contains(t, resBody.Message, "does not accept application requests on this hostname") + require.Contains(t, string(body), "does not accept application requests on this hostname") }) t.Run("InvalidSubdomain", func(t *testing.T) { @@ -547,12 +545,11 @@ func TestWorkspaceAppsProxySubdomainBlocked(t *testing.T) { require.NoError(t, err) defer resp.Body.Close() - // Should have an error response. + // Should have a HTML error response. require.Equal(t, http.StatusBadRequest, resp.StatusCode) - var resBody codersdk.Response - err = json.NewDecoder(resp.Body).Decode(&resBody) + body, err := io.ReadAll(resp.Body) require.NoError(t, err) - require.Contains(t, resBody.Message, "Could not parse subdomain application URL") + require.Contains(t, string(body), "Could not parse subdomain application URL") }) } diff --git a/site/index.html b/site/index.html index 430a0e723dcdf..312dcdcf0a36b 100644 --- a/site/index.html +++ b/site/index.html @@ -16,11 +16,6 @@ - import("./pages/WorkspacePage/WorkspacePage")) const WorkspaceSchedulePage = lazy( () => import("./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"), ) -const WorkspaceAppErrorPage = lazy( - () => import("./pages/WorkspaceAppErrorPage/WorkspaceAppErrorPage"), -) const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage")) const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage")) const TemplatePage = lazy(() => import("./pages/TemplatePage/TemplatePage")) @@ -189,17 +186,6 @@ export const AppRouter: FC = () => { } /> - - - - - } - /> - - { - const { app } = useParams() - const message = useMemo(() => { - const tag = document.getElementById("api-response") - if (!tag) { - throw new Error("dev error: api-response meta tag not found") - } - return tag.getAttribute("data-message") as string - }, []) - - return -} - -export default WorkspaceAppErrorView diff --git a/site/src/pages/WorkspaceAppErrorPage/WorkspaceAppErrorPageView.stories.tsx b/site/src/pages/WorkspaceAppErrorPage/WorkspaceAppErrorPageView.stories.tsx deleted file mode 100644 index e2aa38ab529a3..0000000000000 --- a/site/src/pages/WorkspaceAppErrorPage/WorkspaceAppErrorPageView.stories.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Story } from "@storybook/react" -import { - WorkspaceAppErrorPageView, - WorkspaceAppErrorPageViewProps, -} from "./WorkspaceAppErrorPageView" - -export default { - title: "pages/WorkspaceAppErrorPageView", - component: WorkspaceAppErrorPageView, -} - -const Template: Story = (args) => ( - -) - -export const NotRunning = Template.bind({}) -NotRunning.args = { - appName: "code-server", - // This is a real message copied and pasted from the backend! - message: - "remote dial error: dial 'tcp://localhost:13337': dial tcp 127.0.0.1:13337: connect: connection refused", -} diff --git a/site/src/pages/WorkspaceAppErrorPage/WorkspaceAppErrorPageView.tsx b/site/src/pages/WorkspaceAppErrorPage/WorkspaceAppErrorPageView.tsx deleted file mode 100644 index f1fd4af90f171..0000000000000 --- a/site/src/pages/WorkspaceAppErrorPage/WorkspaceAppErrorPageView.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { makeStyles } from "@material-ui/core/styles" -import { FC } from "react" - -export interface WorkspaceAppErrorPageViewProps { - appName: string - message: string -} - -export const WorkspaceAppErrorPageView: FC< - React.PropsWithChildren -> = (props) => { - const styles = useStyles() - - return ( -
-

{props.appName} is offline!

-

{props.message}

-
- ) -} - -const useStyles = makeStyles((theme) => ({ - root: { - flex: 1, - padding: theme.spacing(10), - }, - title: { - textAlign: "center", - }, - message: { - textAlign: "center", - }, -})) 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