Skip to content

Commit 80b5600

Browse files
committed
Add embed errors
1 parent e9b7463 commit 80b5600

File tree

9 files changed

+187
-31
lines changed

9 files changed

+187
-31
lines changed

coderd/coderd.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,9 @@ func New(options *Options) *API {
7676

7777
r := chi.NewRouter()
7878
api := &API{
79-
Options: options,
80-
Handler: r,
79+
Options: options,
80+
Handler: r,
81+
siteHandler: site.Handler(),
8182
}
8283
api.workspaceAgentCache = wsconncache.New(api.dialWorkspaceAgent, 0)
8384

@@ -95,14 +96,19 @@ func New(options *Options) *API {
9596
tracing.HTTPMW(api.TracerProvider, "coderd.http"),
9697
)
9798

98-
r.Route("/@{user}/{workspaceagent}/apps/{application}", func(r chi.Router) {
99+
apps := func(r chi.Router) {
99100
r.Use(
100101
httpmw.RateLimitPerMinute(options.APIRateLimit),
101102
apiKeyMiddleware,
102103
httpmw.ExtractUserParam(api.Database),
103104
)
104105
r.Get("/*", api.workspaceAppsProxyPath)
105-
})
106+
}
107+
// %40 is the encoded character of the @ symbol. VS Code Web does
108+
// not handle character encoding properly, so it's safe to assume
109+
// other applications might not as well.
110+
r.Route("/%40{user}/{workspaceagent}/apps/{application}", apps)
111+
r.Route("/@{user}/{workspaceagent}/apps/{application}", apps)
106112

107113
r.Route("/api/v2", func(r chi.Router) {
108114
r.NotFound(func(rw http.ResponseWriter, r *http.Request) {
@@ -338,14 +344,15 @@ func New(options *Options) *API {
338344
r.Get("/state", api.workspaceBuildState)
339345
})
340346
})
341-
r.NotFound(site.DefaultHandler().ServeHTTP)
347+
r.NotFound(api.siteHandler.ServeHTTP)
342348
return api
343349
}
344350

345351
type API struct {
346352
*Options
347353

348354
Handler chi.Router
355+
siteHandler http.Handler
349356
websocketWaitMutex sync.Mutex
350357
websocketWaitGroup sync.WaitGroup
351358
workspaceAgentCache *wsconncache.Cache

coderd/workspaceapps.go

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/coder/coder/coderd/database"
1616
"github.com/coder/coder/coderd/httpapi"
1717
"github.com/coder/coder/coderd/httpmw"
18+
"github.com/coder/coder/site"
1819
)
1920

2021
func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request) {
@@ -113,23 +114,15 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
113114
return
114115
}
115116

116-
conn, release, err := api.workspaceAgentCache.Acquire(r, agent.ID)
117-
if err != nil {
118-
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
119-
Message: fmt.Sprintf("dial workspace agent: %s", err),
120-
})
121-
return
122-
}
123-
defer release()
124-
125117
proxy := httputil.NewSingleHostReverseProxy(appURL)
126-
// Write the error directly using our format!
118+
// Write an error using our embed handler
127119
proxy.ErrorHandler = func(w http.ResponseWriter, r *http.Request, err error) {
128-
httpapi.Write(w, http.StatusBadGateway, httpapi.Response{
129-
Message: err.Error(),
130-
})
120+
r = r.WithContext(site.WithAPIResponse(r.Context(), site.APIResponse{
121+
StatusCode: http.StatusBadGateway,
122+
Message: err.Error(),
123+
}))
124+
api.siteHandler.ServeHTTP(w, r)
131125
}
132-
proxy.Transport = conn.HTTPTransport()
133126
path := chi.URLParam(r, "*")
134127
if !strings.HasSuffix(r.URL.Path, "/") && path == "" {
135128
// Web applications typically request paths relative to the
@@ -139,6 +132,27 @@ func (api *API) workspaceAppsProxyPath(rw http.ResponseWriter, r *http.Request)
139132
http.Redirect(rw, r, r.URL.String(), http.StatusTemporaryRedirect)
140133
return
141134
}
135+
if r.URL.RawQuery == "" && appURL.RawQuery != "" {
136+
// If the application defines a default set of query parameters,
137+
// we should always respect them. The reverse proxy will merge
138+
// query parameters for server-side requests, but sometimes
139+
// client-side applications require the query parameters to render
140+
// properly. With code-server, this is the "folder" param.
141+
r.URL.RawQuery = appURL.RawQuery
142+
http.Redirect(rw, r, r.URL.String(), http.StatusTemporaryRedirect)
143+
return
144+
}
142145
r.URL.Path = path
146+
147+
conn, release, err := api.workspaceAgentCache.Acquire(r, agent.ID)
148+
if err != nil {
149+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
150+
Message: fmt.Sprintf("dial workspace agent: %s", err),
151+
})
152+
return
153+
}
154+
defer release()
155+
156+
proxy.Transport = conn.HTTPTransport()
143157
proxy.ServeHTTP(rw, r)
144158
}

coderd/workspaceapps_test.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
3838

3939
client, coderAPI := coderdtest.NewWithAPI(t, nil)
4040
user := coderdtest.CreateFirstUser(t, client)
41-
daemonCloser := coderdtest.NewProvisionerDaemon(t, coderAPI)
41+
coderdtest.NewProvisionerDaemon(t, coderAPI)
4242
authToken := uuid.NewString()
4343
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
4444
Parse: echo.ParseComplete,
@@ -56,7 +56,7 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
5656
},
5757
Apps: []*proto.App{{
5858
Name: "example",
59-
Url: fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port),
59+
Url: fmt.Sprintf("http://127.0.0.1:%d?query=true", tcpAddr.Port),
6060
}},
6161
}},
6262
}},
@@ -68,7 +68,6 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
6868
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
6969
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
7070
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
71-
daemonCloser.Close()
7271

7372
agentClient := codersdk.New(client.URL)
7473
agentClient.SessionToken = authToken
@@ -91,11 +90,22 @@ func TestWorkspaceAppsProxyPath(t *testing.T) {
9190
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
9291
})
9392

94-
t.Run("Proxies", func(t *testing.T) {
93+
t.Run("RedirectsWithQuery", func(t *testing.T) {
9594
t.Parallel()
9695
resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/example/", nil)
9796
require.NoError(t, err)
9897
defer resp.Body.Close()
98+
require.Equal(t, http.StatusTemporaryRedirect, resp.StatusCode)
99+
loc, err := resp.Location()
100+
require.NoError(t, err)
101+
require.Equal(t, "query=true", loc.RawQuery)
102+
})
103+
104+
t.Run("Proxies", func(t *testing.T) {
105+
t.Parallel()
106+
resp, err := client.Request(context.Background(), http.MethodGet, "/@me/"+workspace.Name+"/apps/example/?query=true", nil)
107+
require.NoError(t, err)
108+
defer resp.Body.Close()
99109
body, err := io.ReadAll(resp.Body)
100110
require.NoError(t, err)
101111
require.Equal(t, "", string(body))
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
---
2+
name: Develop code-server in Docker
3+
description: Run code-server in a Docker development environment
4+
tags: [local, docker]
5+
---
6+
7+
# code-server in Docker
8+
9+
## Getting started
10+
11+
Run `coder templates init` and select this template. Follow the instructions that appear.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
terraform {
2+
required_providers {
3+
coder = {
4+
source = "coder/coder"
5+
version = "0.4.2"
6+
}
7+
docker = {
8+
source = "kreuzwerker/docker"
9+
version = "~> 2.16.0"
10+
}
11+
}
12+
}
13+
14+
provider "coder" {
15+
}
16+
17+
data "coder_workspace" "me" {
18+
}
19+
20+
resource "coder_agent" "dev" {
21+
arch = "amd64"
22+
os = "linux"
23+
startup_script = "code-server --auth none"
24+
}
25+
26+
resource "coder_app" "code-server" {
27+
agent_id = coder_agent.dev.id
28+
url = "http://localhost:8080/?folder=/home/coder"
29+
}
30+
31+
resource "docker_container" "workspace" {
32+
count = data.coder_workspace.me.start_count
33+
image = "codercom/code-server:latest"
34+
# Uses lower() to avoid Docker restriction on container names.
35+
name = "coder-${data.coder_workspace.me.owner}-${lower(data.coder_workspace.me.name)}"
36+
hostname = lower(data.coder_workspace.me.name)
37+
dns = ["1.1.1.1"]
38+
# Use the docker gateway if the access URL is 127.0.0.1
39+
entrypoint = ["sh", "-c", replace(coder_agent.dev.init_script, "127.0.0.1", "host.docker.internal")]
40+
env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"]
41+
host {
42+
host = "host.docker.internal"
43+
ip = "host-gateway"
44+
}
45+
}

site/embed.go

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package site
55

66
import (
77
"bytes"
8+
"context"
89
"embed"
910
"fmt"
1011
"io"
@@ -28,7 +29,16 @@ import (
2829
//go:embed out/bin/*
2930
var site embed.FS
3031

31-
func DefaultHandler() http.Handler {
32+
type apiResponseContextKey struct{}
33+
34+
// WithAPIResponse returns a context with the APIResponse value attached.
35+
// This is used to inject API response data to the index.html for additional
36+
// metadata in error pages.
37+
func WithAPIResponse(ctx context.Context, apiResponse APIResponse) context.Context {
38+
return context.WithValue(ctx, apiResponseContextKey{}, apiResponse)
39+
}
40+
41+
func Handler() http.Handler {
3242
// the out directory is where webpack builds are created. It is in the same
3343
// directory as this file (package site).
3444
siteFS, err := fs.Sub(site, "out")
@@ -38,11 +48,11 @@ func DefaultHandler() http.Handler {
3848
panic(err)
3949
}
4050

41-
return Handler(siteFS)
51+
return HandlerWithFS(siteFS)
4252
}
4353

4454
// Handler returns an HTTP handler for serving the static site.
45-
func Handler(fileSystem fs.FS) http.Handler {
55+
func HandlerWithFS(fileSystem fs.FS) http.Handler {
4656
// html files are handled by a text/template. Non-html files
4757
// are served by the default file server.
4858
//
@@ -90,8 +100,14 @@ func (h *handler) exists(filePath string) bool {
90100
}
91101

92102
type htmlState struct {
93-
CSP cspState
94-
CSRF csrfState
103+
APIResponse APIResponse
104+
CSP cspState
105+
CSRF csrfState
106+
}
107+
108+
type APIResponse struct {
109+
StatusCode int
110+
Message string
95111
}
96112

97113
type cspState struct {
@@ -139,6 +155,11 @@ func (h *handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
139155
CSRF: csrfState{Token: nosurf.Token(req)},
140156
}
141157

158+
apiResponseRaw := req.Context().Value(apiResponseContextKey{})
159+
if apiResponseRaw != nil {
160+
state.APIResponse = apiResponseRaw.(APIResponse)
161+
}
162+
142163
// First check if it's a file we have in our templates
143164
if h.serveHTML(resp, req, reqFile, state) {
144165
return

site/embed_slim.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ import (
77
"net/http"
88
)
99

10-
func DefaultHandler() http.Handler {
10+
type APIResponse struct {
11+
StatusCode int
12+
Message string
13+
}
14+
15+
func Handler() http.Handler {
1116
return http.NotFoundHandler()
1217
}
18+
19+
func WithAPIResponse(ctx context.Context, _ APIResponse) context.Context {
20+
return ctx
21+
}

site/embed_test.go

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package site_test
55

66
import (
77
"context"
8+
"encoding/json"
89
"fmt"
910
"io"
1011
"net/http"
@@ -39,7 +40,7 @@ func TestCaching(t *testing.T) {
3940
},
4041
}
4142

42-
srv := httptest.NewServer(site.Handler(rootFS))
43+
srv := httptest.NewServer(site.HandlerWithFS(rootFS))
4344
defer srv.Close()
4445

4546
// Create a context
@@ -98,7 +99,7 @@ func TestServingFiles(t *testing.T) {
9899
},
99100
}
100101

101-
srv := httptest.NewServer(site.Handler(rootFS))
102+
srv := httptest.NewServer(site.HandlerWithFS(rootFS))
102103
defer srv.Close()
103104

104105
// Create a context
@@ -172,3 +173,40 @@ func TestShouldCacheFile(t *testing.T) {
172173
require.Equal(t, testCase.expected, got, fmt.Sprintf("Expected ShouldCacheFile(%s) to be %t", testCase.reqFile, testCase.expected))
173174
}
174175
}
176+
177+
func TestServeAPIResponse(t *testing.T) {
178+
t.Parallel()
179+
180+
// Create a test server
181+
rootFS := fstest.MapFS{
182+
"index.html": &fstest.MapFile{
183+
Data: []byte(`{"code":{{ .APIResponse.StatusCode }},"message":"{{ .APIResponse.Message }}"}`),
184+
},
185+
}
186+
187+
apiResponse := site.APIResponse{
188+
StatusCode: http.StatusBadGateway,
189+
Message: "This could be an error message!",
190+
}
191+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
192+
r = r.WithContext(site.WithAPIResponse(r.Context(), apiResponse))
193+
site.HandlerWithFS(rootFS).ServeHTTP(w, r)
194+
}))
195+
defer srv.Close()
196+
197+
req, err := http.NewRequestWithContext(context.Background(), "GET", srv.URL, nil)
198+
require.NoError(t, err)
199+
resp, err := http.DefaultClient.Do(req)
200+
require.NoError(t, err)
201+
var body struct {
202+
Code int `json:"code"`
203+
Message string `json:"message"`
204+
}
205+
data, err := io.ReadAll(resp.Body)
206+
require.NoError(t, err)
207+
t.Logf("resp: %q", data)
208+
err = json.Unmarshal(data, &body)
209+
require.NoError(t, err)
210+
require.Equal(t, apiResponse.StatusCode, body.Code)
211+
require.Equal(t, apiResponse.Message, body.Message)
212+
}

site/htmlTemplates/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
<meta property="og:type" content="website" />
1818
<meta property="csp-nonce" content="{{ .CSP.Nonce }}" />
1919
<meta property="csrf-token" content="{{ .CSRF.Token }}" />
20+
<meta id="api-response" data-statuscode="{{ .APIResponse.StatusCode }}" data-message="{{ .APIResponse.Message }}" />
2021
<link rel="mask-icon" href="/static/favicon.svg" color="#000000" crossorigin="use-credentials" />
2122
<link rel="alternate icon" type="image/png" href="/favicon.png" />
2223
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />

0 commit comments

Comments
 (0)
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