Skip to content

Commit 506a28a

Browse files
committed
add custom 503 pages for when backends are down
1 parent e977957 commit 506a28a

3 files changed

Lines changed: 226 additions & 0 deletions

File tree

internal/xds/503-dashboard.html

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>503 Dashboard Unavailable</title>
5+
<style>
6+
* {
7+
box-sizing: border-box;
8+
margin: 0;
9+
padding: 0;
10+
}
11+
body {
12+
font-family:
13+
system-ui,
14+
-apple-system,
15+
sans-serif;
16+
background: #0a0a0a;
17+
color: #e5e5e5;
18+
display: flex;
19+
align-items: center;
20+
justify-content: center;
21+
min-height: 100vh;
22+
padding: 1rem;
23+
}
24+
.container {
25+
text-align: center;
26+
max-width: 500px;
27+
}
28+
h1 {
29+
font-size: 4rem;
30+
color: #ef4444;
31+
margin-bottom: 1rem;
32+
}
33+
h2 {
34+
font-size: 1.5rem;
35+
margin-bottom: 1rem;
36+
color: #fff;
37+
}
38+
p {
39+
color: #b8b8b8;
40+
line-height: 1.6;
41+
}
42+
code {
43+
background: #262626;
44+
padding: 0.2rem 0.4rem;
45+
border-radius: 0.25rem;
46+
font-size: 0.9rem;
47+
}
48+
</style>
49+
</head>
50+
<body>
51+
<div class="container">
52+
<h1>503</h1>
53+
<h2>Dashboard Unavailable</h2>
54+
<p>
55+
The dashboard is down but envoy isn't. You might need to restart
56+
localproxy after force killing envoy with
57+
<code>sudo killall envoy</code>
58+
</p>
59+
</div>
60+
</body>
61+
</html>

internal/xds/503.html

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<title>503 Service Unavailable</title>
5+
<style>
6+
* {
7+
box-sizing: border-box;
8+
margin: 0;
9+
padding: 0;
10+
}
11+
body {
12+
font-family:
13+
system-ui,
14+
-apple-system,
15+
sans-serif;
16+
background: #0a0a0a;
17+
color: #e5e5e5;
18+
display: flex;
19+
align-items: center;
20+
justify-content: center;
21+
min-height: 100vh;
22+
padding: 1rem;
23+
}
24+
.container {
25+
text-align: center;
26+
max-width: 500px;
27+
}
28+
h1 {
29+
font-size: 4rem;
30+
color: #ef4444;
31+
margin-bottom: 1rem;
32+
}
33+
h2 {
34+
font-size: 1.5rem;
35+
margin-bottom: 1rem;
36+
color: #fff;
37+
}
38+
p {
39+
color: #b8b8b8;
40+
line-height: 1.6;
41+
}
42+
</style>
43+
</head>
44+
<body>
45+
<div class="container">
46+
<h1>503</h1>
47+
<h2>Service Unavailable</h2>
48+
<p>
49+
The backend service is not responding. Please check that your
50+
application is running and try again.
51+
</p>
52+
</div>
53+
</body>
54+
</html>

internal/xds/snapshot.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package xds
22

33
import (
4+
_ "embed"
45
"fmt"
56
"net"
67
"time"
78

9+
accesslog "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3"
810
cluster "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
911
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
1012
endpoint "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
@@ -21,8 +23,15 @@ import (
2123
"github.com/envoyproxy/go-control-plane/pkg/resource/v3"
2224
"google.golang.org/protobuf/types/known/anypb"
2325
"google.golang.org/protobuf/types/known/durationpb"
26+
"google.golang.org/protobuf/types/known/wrapperspb"
2427
)
2528

29+
//go:embed 503.html
30+
var error503HTML string
31+
32+
//go:embed 503-dashboard.html
33+
var error503DashboardHTML string
34+
2635
type Protocol string
2736

2837
const (
@@ -89,12 +98,112 @@ func (b *SnapshotBuilder) Build(routes []Route, httpsRedirect bool) (*cache.Snap
8998

9099
routerFilter, _ := anypb.New(&router.Router{})
91100

101+
statusCode503Filter := &accesslog.AccessLogFilter{
102+
FilterSpecifier: &accesslog.AccessLogFilter_StatusCodeFilter{
103+
StatusCodeFilter: &accesslog.StatusCodeFilter{
104+
Comparison: &accesslog.ComparisonFilter{
105+
Op: accesslog.ComparisonFilter_EQ,
106+
Value: &core.RuntimeUInt32{
107+
DefaultValue: 503,
108+
RuntimeKey: "local_reply_503",
109+
},
110+
},
111+
},
112+
},
113+
}
114+
115+
dashboardHostFilter := &accesslog.AccessLogFilter{
116+
FilterSpecifier: &accesslog.AccessLogFilter_OrFilter{
117+
OrFilter: &accesslog.OrFilter{
118+
Filters: []*accesslog.AccessLogFilter{
119+
{
120+
FilterSpecifier: &accesslog.AccessLogFilter_HeaderFilter{
121+
HeaderFilter: &accesslog.HeaderFilter{
122+
Header: &route.HeaderMatcher{
123+
Name: ":authority",
124+
HeaderMatchSpecifier: &route.HeaderMatcher_ExactMatch{
125+
ExactMatch: "localhost",
126+
},
127+
},
128+
},
129+
},
130+
},
131+
{
132+
FilterSpecifier: &accesslog.AccessLogFilter_HeaderFilter{
133+
HeaderFilter: &accesslog.HeaderFilter{
134+
Header: &route.HeaderMatcher{
135+
Name: ":authority",
136+
HeaderMatchSpecifier: &route.HeaderMatcher_ExactMatch{
137+
ExactMatch: "proxy.localhost",
138+
},
139+
},
140+
},
141+
},
142+
},
143+
{
144+
FilterSpecifier: &accesslog.AccessLogFilter_HeaderFilter{
145+
HeaderFilter: &accesslog.HeaderFilter{
146+
Header: &route.HeaderMatcher{
147+
Name: ":authority",
148+
HeaderMatchSpecifier: &route.HeaderMatcher_ExactMatch{
149+
ExactMatch: "proxy.internal",
150+
},
151+
},
152+
},
153+
},
154+
},
155+
},
156+
},
157+
},
158+
}
159+
160+
localReplyConfig := &hcm.LocalReplyConfig{
161+
Mappers: []*hcm.ResponseMapper{
162+
{
163+
Filter: &accesslog.AccessLogFilter{
164+
FilterSpecifier: &accesslog.AccessLogFilter_AndFilter{
165+
AndFilter: &accesslog.AndFilter{
166+
Filters: []*accesslog.AccessLogFilter{
167+
statusCode503Filter,
168+
dashboardHostFilter,
169+
},
170+
},
171+
},
172+
},
173+
StatusCode: wrapperspb.UInt32(503),
174+
Body: &core.DataSource{
175+
Specifier: &core.DataSource_InlineString{InlineString: error503DashboardHTML},
176+
},
177+
BodyFormatOverride: &core.SubstitutionFormatString{
178+
ContentType: "text/html; charset=UTF-8",
179+
Format: &core.SubstitutionFormatString_TextFormat{
180+
TextFormat: "%LOCAL_REPLY_BODY%",
181+
},
182+
},
183+
},
184+
{
185+
Filter: statusCode503Filter,
186+
StatusCode: wrapperspb.UInt32(503),
187+
Body: &core.DataSource{
188+
Specifier: &core.DataSource_InlineString{InlineString: error503HTML},
189+
},
190+
BodyFormatOverride: &core.SubstitutionFormatString{
191+
ContentType: "text/html; charset=UTF-8",
192+
Format: &core.SubstitutionFormatString_TextFormat{
193+
TextFormat: "%LOCAL_REPLY_BODY%",
194+
},
195+
},
196+
},
197+
},
198+
}
199+
92200
httpsHcm := &hcm.HttpConnectionManager{
93201
CodecType: hcm.HttpConnectionManager_AUTO,
94202
StatPrefix: "https_ingress",
95203
StreamIdleTimeout: durationpb.New(0),
96204
RequestTimeout: durationpb.New(0),
97205
RequestHeadersTimeout: durationpb.New(0),
206+
LocalReplyConfig: localReplyConfig,
98207
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
99208
Rds: &hcm.Rds{
100209
ConfigSource: &core.ConfigSource{
@@ -119,6 +228,7 @@ func (b *SnapshotBuilder) Build(routes []Route, httpsRedirect bool) (*cache.Snap
119228
StreamIdleTimeout: durationpb.New(0),
120229
RequestTimeout: durationpb.New(0),
121230
RequestHeadersTimeout: durationpb.New(0),
231+
LocalReplyConfig: localReplyConfig,
122232
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
123233
Rds: &hcm.Rds{
124234
ConfigSource: &core.ConfigSource{
@@ -387,6 +497,7 @@ func (b *SnapshotBuilder) Build(routes []Route, httpsRedirect bool) (*cache.Snap
387497
StreamIdleTimeout: durationpb.New(0),
388498
RequestTimeout: durationpb.New(0),
389499
RequestHeadersTimeout: durationpb.New(0),
500+
LocalReplyConfig: localReplyConfig,
390501
RouteSpecifier: &hcm.HttpConnectionManager_Rds{
391502
Rds: &hcm.Rds{
392503
ConfigSource: &core.ConfigSource{

0 commit comments

Comments
 (0)