Skip to content

Commit c508716

Browse files
authored
Handle 404 not found properly (#108)
* server: serve 404 status code if page not found * web: add 404 page template
1 parent 8881d16 commit c508716

File tree

3 files changed

+272
-14
lines changed

3 files changed

+272
-14
lines changed

cmd/playground/main.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,20 @@ func start(goRoot string, args appArgs) error {
103103

104104
r := mux.NewRouter()
105105
pg := goplay.NewClient(args.playgroundUrl, goplay.DefaultUserAgent, 15*time.Second)
106+
107+
// API routes
106108
langserver.New(Version, pg, packages, compiler.NewBuildService(zap.S(), store)).
107109
Mount(r.PathPrefix("/api").Subrouter())
108-
r.PathPrefix("/").Handler(langserver.SpaFileServer("./public"))
110+
111+
// Web UI routes
112+
indexHandler := langserver.NewIndexFileServer("./public")
113+
spaHandler := langserver.NewSpaFileServer("./public")
114+
r.Path("/").
115+
Handler(indexHandler)
116+
r.Path("/snippet/{snippetID:[A-Za-z0-9_-]+}").
117+
Handler(indexHandler)
118+
r.PathPrefix("/").
119+
Handler(spaHandler)
109120

110121
var handler http.Handler
111122
if args.debug {

pkg/langserver/spa.go

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,45 @@ import (
88
"strings"
99
)
1010

11-
// Advanced static server
12-
type spaFileServer struct {
11+
const (
12+
IndexFileName = "index.html"
13+
NotFoundFileName = "404.html"
14+
)
15+
16+
type httpStatusInterceptor struct {
17+
http.ResponseWriter
18+
desiredStatus int
19+
}
20+
21+
func (i httpStatusInterceptor) WriteHeader(_ int) {
22+
i.ResponseWriter.WriteHeader(i.desiredStatus)
23+
}
24+
25+
type IndexFileServer struct {
26+
indexFilePath string
27+
}
28+
29+
// NewIndexFileServer returns handler which serves index.html page from root.
30+
func NewIndexFileServer(root http.Dir) *IndexFileServer {
31+
return &IndexFileServer{
32+
indexFilePath: filepath.Join(string(root), IndexFileName),
33+
}
34+
}
35+
36+
func (fs IndexFileServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
37+
http.ServeFile(rw, r, fs.indexFilePath)
38+
}
39+
40+
// SpaFileServer is a wrapper around http.FileServer for serving SPA contents.
41+
type SpaFileServer struct {
1342
root http.Dir
14-
NotFoundHandler func(http.ResponseWriter, *http.Request)
43+
NotFoundHandler http.Handler
1544
}
1645

1746
// ServeHTTP implements http.Handler
18-
func (fs *spaFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
47+
func (fs *SpaFileServer) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
1948
if containsDotDot(r.URL.Path) {
20-
Errorf(http.StatusNotFound, "Not Found").WriteResponse(w)
49+
Errorf(http.StatusNotFound, "Not Found").WriteResponse(rw)
2150
return
2251
}
2352

@@ -39,16 +68,14 @@ func (fs *spaFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
3968
name := path.Join(dir, filepath.FromSlash(upath))
4069

4170
//check if file exists
42-
f, err := os.Open(name)
43-
if err != nil {
71+
if _, err := os.Stat(name); err != nil {
4472
if os.IsNotExist(err) {
45-
http.ServeFile(w, r, string(fs.root)+"/index.html")
73+
fs.NotFoundHandler.ServeHTTP(rw, r)
4674
return
4775
}
4876
}
49-
defer f.Close()
5077

51-
http.ServeFile(w, r, name)
78+
http.ServeFile(rw, r, name)
5279
}
5380

5481
func containsDotDot(v string) bool {
@@ -65,7 +92,24 @@ func containsDotDot(v string) bool {
6592

6693
func isSlashRune(r rune) bool { return r == '/' || r == '\\' }
6794

68-
// SpaFileServer returns SPA handler
69-
func SpaFileServer(root http.Dir) http.Handler {
70-
return &spaFileServer{root: root}
95+
// NewSpaFileServer returns SPA handler
96+
func NewSpaFileServer(root http.Dir) *SpaFileServer {
97+
notFoundHandler := NewFileServerWithStatus(filepath.Join(string(root), NotFoundFileName), http.StatusNotFound)
98+
return &SpaFileServer{
99+
NotFoundHandler: notFoundHandler,
100+
root: root,
101+
}
102+
}
103+
104+
// NewFileServerWithStatus returns http.Handler which serves specified file with desired HTTP status
105+
func NewFileServerWithStatus(name string, code int) http.HandlerFunc {
106+
return func(rw http.ResponseWriter, r *http.Request) {
107+
ServeFileWithStatus(rw, r, name, code)
108+
}
109+
}
110+
111+
// ServeFileWithStatus serves file in HTTP response with specified HTTP status.
112+
func ServeFileWithStatus(rw http.ResponseWriter, r *http.Request, name string, code int) {
113+
interceptor := httpStatusInterceptor{desiredStatus: code, ResponseWriter: rw}
114+
http.ServeFile(interceptor, r, name)
71115
}

web/public/404.html

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<title>🤔 404 Not Found!</title>
6+
<style>
7+
body {
8+
background: #1f1f1f;
9+
color: #f4f4f4;
10+
margin: 0;
11+
padding: 0;
12+
font: 11pt system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
13+
}
14+
.errorPage {
15+
display: flex;
16+
justify-content: center;
17+
justify-items: center;
18+
position: fixed;
19+
inset: 0;
20+
flex-direction: row-reverse;
21+
}
22+
.errorPage__container {
23+
align-self: center;
24+
width: 100%;
25+
max-width: 480px;
26+
}
27+
.errorPage__statusCode {
28+
font: bold 6rem SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
29+
margin: 0 0 2rem 0;
30+
padding: 0;
31+
display: block;
32+
-webkit-filter: url(#chromatic-aberration);
33+
filter: url(#chromatic-aberration);
34+
}
35+
.errorPage__statusText {
36+
margin: 0;
37+
padding: 0;
38+
display: block;
39+
font-size: 2rem;
40+
}
41+
.errorPage__message {
42+
margin: 1rem 0;
43+
color: #999;
44+
}
45+
.errorPage__message p {
46+
margin: 0 0 .3rem 0;
47+
padding: 0;
48+
}
49+
.errorPage__actions {
50+
margin-top: 4rem;
51+
display: flex;
52+
justify-content: flex-end;
53+
}
54+
.errorPage__actions>* {
55+
margin-left: 1rem;
56+
}
57+
.btn--primary, .btn--transparent {
58+
border: none;
59+
text-decoration: none;
60+
padding: .5rem 1rem;
61+
}
62+
.btn--transparent {
63+
color: #777;
64+
}
65+
.btn--transparent:hover, .btn--transparent:focus {
66+
color: inherit;
67+
}
68+
.btn--primary {
69+
color: #f4f4f4;
70+
background: #005A9C;
71+
border-radius: 2px;
72+
}
73+
.btn--primary:hover, .btn--primary:focus {
74+
background: #00498B;
75+
}
76+
.gopher {
77+
height: 320px;
78+
align-self: center;
79+
margin-right: 2.5rem;
80+
}
81+
.gopher__image {
82+
height: 100%;
83+
}
84+
</style>
85+
</head>
86+
<body>
87+
<div class="errorPage">
88+
<div class="errorPage__container">
89+
<h1 class="errorPage__statusCode">404!</h1>
90+
<h2 class="errorPage__statusText">Page Not Found</h2>
91+
<div class="errorPage__message">
92+
<p>
93+
Requested page does not exist or was deleted.
94+
</p>
95+
<p>
96+
That's all we know 🤷
97+
</p>
98+
</div>
99+
<div class="errorPage__actions">
100+
<a href="//http.cat/404" class="btn--transparent">Show me cats</a>
101+
<a href="/" class="btn--primary">Go To Home</a>
102+
</div>
103+
</div>
104+
<div class="gopher">
105+
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px"
106+
y="0px" width="236.7px" height="320px" viewBox="0 0 438.004 592.145"
107+
enable-background="new 0 0 236.7 320"
108+
class="gopher__image"
109+
xml:space="preserve">
110+
<g>
111+
<path fill="#FFFFFF" stroke="#000000" stroke-width="6" stroke-linecap="round"
112+
stroke-linejoin="round" stroke-miterlimit="10" d="
113+
M45.352,291.011c-13.378,1.876-28.958,5.754-37.066,10.684c-7.337,4.463-6.374,17.6-1.154,24.099
114+
c5.509,6.862,11.974,6.495,19.779,2.905c5.123-2.357,11.293-7.609,17.667-13.492c0.16-5.126,0.339-10.256,0.543-15.375
115+
C45.242,296.793,45.317,293.857,45.352,291.011z"/>
116+
117+
<path fill="#FFFFFF" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
118+
M429.717,297.195c-7.5-4.56-21.389-8.217-34.018-10.226c0.316,6.816,0.751,14.31,1.339,22.763c0.106,1.527,0.211,3.033,0.313,4.529
119+
c4.959,4.379,9.68,8.068,13.74,9.937c7.807,3.591,14.27,3.958,19.779-2.904C436.091,314.795,437.055,301.656,429.717,297.195z"/>
120+
121+
<path fill="#FFFFFF" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
122+
M55.17,564.844c-9.854,17.521,15.9,31.951,29.25,19.801c10.908-9.928,19.518-18.348,38.256-21.098
123+
c-14.35-5.906-27-13.281-37.752-21.871C71.807,546.805,61.188,554.146,55.17,564.844z"/>
124+
125+
<path fill="#FFFFFF" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
126+
M353.745,579.846c11.037,14.283,38.46,1.312,30.443-17.121c-5.267-12.111-15.218-22.156-27.677-28.195
127+
c-8.158,7.618-17.849,14.689-29.308,20.867C339.883,561.402,346.096,569.946,353.745,579.846z"/>
128+
<path fill="none" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
129+
M5.048,322.396c3.333-2.397,5.688-4.991,11.784-8.097"/>
130+
<path fill="none" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
131+
M432.955,317.896c-3.333-2.397-5.688-4.991-11.783-8.097"/>
132+
<path fill="none" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
133+
M60.871,584.57c0.9-4.951,3.6-13.275,11.7-20.476"/>
134+
<path fill="none" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
135+
M376.05,582.961c-0.026-5.031-1.304-15.266-8.866-22.195"/>
136+
137+
<path fill="#74cddd" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
138+
M41.071,74.477c0-6.969,5.641-12.618,12.6-12.618c4.081,0,7.697,1.951,10,4.964c5.569-9.111,12.283-16.97,19.913-23.729
139+
c-3.78-3.658-9.922-6.33-19.563-7.625c-31.05-4.174-46.917,11.166-50.542,31.725c-3.498,19.838,7.171,41.143,31.004,46.682
140+
c2.46-9.685,5.474-18.569,8.95-26.793C46.585,86.954,41.071,81.365,41.071,74.477z"/>
141+
<path d="M53.671,61.859c-6.959,0-12.6,5.649-12.6,12.618c0,6.888,5.514,12.477,12.362,12.605
142+
c2.479-5.866,5.191-11.399,8.112-16.638c0.687-1.231,1.4-2.434,2.126-3.622C61.368,63.81,57.751,61.859,53.671,61.859z"/>
143+
144+
<path fill="#74cddd" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
145+
M413.979,59.094c-3.625-20.56-19.492-35.899-50.541-31.725c-10.765,1.447-17.166,4.61-20.789,8.945
146+
c6.867,5.93,13.257,12.745,19.272,20.518c0.292,0.377,0.57,0.758,0.857,1.136c2.296-3.095,5.963-5.109,10.108-5.109
147+
c6.959,0,12.6,5.649,12.6,12.618c0,6.109-4.335,11.202-10.093,12.366c4.277,8.283,7.745,17.199,10.521,27.142
148+
C407.677,98.434,417.33,78.095,413.979,59.094z"/>
149+
<path d="M385.486,65.477c0-6.969-5.641-12.618-12.6-12.618c-4.146,0-7.812,2.014-10.108,5.109
150+
c4.815,6.345,9.007,12.886,12.615,19.875C381.151,76.679,385.486,71.586,385.486,65.477z"/>
151+
152+
<path fill="#74cddd" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
153+
M362.778,57.968c-0.287-0.378-0.565-0.759-0.857-1.136c-6.016-7.773-12.405-14.588-19.272-20.518
154+
c-29.797-25.728-68.582-34.79-124.728-33.123c-48.682,1.447-100.473,9.906-134.337,39.903c-7.63,6.758-14.343,14.617-19.913,23.729
155+
c-0.726,1.188-1.439,2.391-2.126,3.622c-2.921,5.239-5.633,10.771-8.112,16.638c-3.476,8.224-6.49,17.108-8.95,26.793
156+
c-4.767,18.77-7.463,40.533-7.462,66.257c0.002,45.133,8.866,67.528,8.332,110.879c-0.035,2.846-0.11,5.782-0.231,8.821
157+
c-0.204,5.119-0.383,10.249-0.543,15.375c-1.653,53.107-1.062,105.862-1.499,142.036c-0.401,33.204,14.646,62.704,41.845,84.433
158+
c10.752,8.59,23.402,15.965,37.752,21.871c25.113,10.337,55.418,16.186,89.844,16.186c50.265,0,87.456-9.652,114.684-24.336
159+
c11.459-6.178,21.149-13.249,29.308-20.867c20.359-19.008,31.17-41.422,36.009-61.896c11.47-48.523,9.966-84.08,4.831-158.371
160+
c-0.103-1.496-0.207-3.002-0.313-4.529c-0.588-8.453-1.022-15.947-1.339-22.763c-1.733-37.343,0.064-54.317-0.479-96.937
161+
c-0.463-36.271-3.195-63.161-9.306-85.047c-2.776-9.942-6.244-18.858-10.521-27.142C371.785,70.854,367.594,64.312,362.778,57.968z
162+
"/>
163+
164+
<ellipse fill="#FFFFFF" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" cx="144.121" cy="91.595" rx="54.9" ry="53.1"/>
165+
<circle cx="115.321" cy="94.294" r="18.9"/>
166+
<circle fill="#FFFFFF" cx="123.421" cy="98.794" r="5.4"/>
167+
168+
<ellipse fill="#FFFFFF" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" cx="281.821" cy="85.294" rx="54.9" ry="53.1"/>
169+
<circle cx="253.021" cy="87.995" r="18.9"/>
170+
<circle fill="#FFFFFF" cx="261.121" cy="92.495" r="5.4"/>
171+
172+
<path fill="#FFFFFF" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
173+
M214.968,170.343c-10.784,0.188-12.401,4.999-21.685,6.657c-0.328,5.727-0.745,13.566-1.163,22.896c-0.9,20.1,24.6,15.6,24.6,3.6
174+
c0-9.129,0-25.714,0-33.132C216.16,170.343,215.583,170.333,214.968,170.343z"/>
175+
176+
<path fill="#FFFFFF" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
177+
M216.72,203.495c0,12,25.5,16.5,24.6-3.6c-0.429-9.582-0.857-17.59-1.189-23.353c-11.689-0.533-13.115-5.813-23.411-6.179
178+
C216.72,177.78,216.72,194.365,216.72,203.495z"/>
179+
180+
<path fill="#AC967B" stroke="#000000" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="
181+
M254.674,150.297c-4.354-4.685-9.521-7.238-16.425-8.471c-3.664,5.366-12.791,9.169-23.479,9.169
182+
c-10.278,0-19.112-3.518-23.034-8.56c-7.391,1.417-12.753,4.185-17.15,9.26c-8.627,9.959-4.437,24.891,7.156,25.695
183+
c4.957,0.344,8.624,0.131,11.541-0.391c9.284-1.658,10.901-6.469,21.685-6.657c0.615-0.01,1.192,0,1.752,0.02
184+
c10.295,0.366,11.721,5.646,23.411,6.179c2.312,0.105,5.024,0.026,8.289-0.316C259.979,175.018,263.645,159.949,254.674,150.297z"
185+
/>
186+
<path d="M214.771,150.995c10.688,0,19.814-3.803,23.479-9.169c1.107-1.622,1.722-3.385,1.722-5.231c0-7.953-11.281-14.4-25.2-14.4
187+
c-13.917,0-25.2,6.447-25.2,14.4c0,2.08,0.778,4.054,2.166,5.84C195.658,147.477,204.493,150.995,214.771,150.995z"/>
188+
</g>
189+
</svg>
190+
191+
</div>
192+
</div>
193+
<svg width="0" height="0">
194+
<filter id="chromatic-aberration">
195+
<feColorMatrix type="matrix" result="red_" values="4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0"/>
196+
<feOffset in="red_" dx="2" dy="0" result="red"/>
197+
<feColorMatrix type="matrix" in="SourceGraphic" result="blue_" values="0 0 0 0 0 0 3 0 0 0 0 0 10 0 0 0 0 0 1 0"/>
198+
<feOffset in="blue_" dx="-3" dy="0" result="blue"/>
199+
<feBlend mode="screen" in="red" in2="blue"/>
200+
</filter>
201+
</svg>
202+
</body>
203+
</html>

0 commit comments

Comments
 (0)