Skip to content

Commit 3d1bdff

Browse files
feat: add fault injection middleware to the server (#9)
This change adds a middleware around all routes in the test server that can be used to trigger faults when serving requests. The middleware is only enabled on requests with `Request-ID` AND `Fault-Settings` headers set. The `Fault-Settings` header is a JSON object which is used to configure whether or not to delay responses, reject requests and/or return HTTP error codes.
1 parent 96f6424 commit 3d1bdff

File tree

4 files changed

+148
-2
lines changed

4 files changed

+148
-2
lines changed

cmd/server/main.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/speakeasy-api/speakeasy-api-test-service/internal/clientcredentials"
1010
"github.com/speakeasy-api/speakeasy-api-test-service/internal/errors"
1111
"github.com/speakeasy-api/speakeasy-api-test-service/internal/eventstreams"
12+
"github.com/speakeasy-api/speakeasy-api-test-service/internal/middleware"
1213
"github.com/speakeasy-api/speakeasy-api-test-service/internal/pagination"
1314
"github.com/speakeasy-api/speakeasy-api-test-service/internal/readonlywriteonly"
1415
"github.com/speakeasy-api/speakeasy-api-test-service/internal/responseHeaders"
@@ -52,13 +53,15 @@ func main() {
5253
r.HandleFunc("/clientcredentials/token", clientcredentials.HandleTokenRequest).Methods(http.MethodPost)
5354
r.HandleFunc("/clientcredentials/authenticatedrequest", clientcredentials.HandleAuthenticatedRequest).Methods(http.MethodPost)
5455

56+
handler := middleware.Fault(r)
57+
5558
bind := ":8080"
5659
if bindArg != nil {
5760
bind = *bindArg
5861
}
5962

6063
log.Printf("Listening on %s\n", bind)
61-
if err := http.ListenAndServe(bind, r); err != nil {
64+
if err := http.ListenAndServe(bind, handler); err != nil {
6265
log.Fatal(err)
6366
}
6467
}

go.mod

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,7 @@ module github.com/speakeasy-api/speakeasy-api-test-service
22

33
go 1.22
44

5-
require github.com/gorilla/mux v1.8.0
5+
require (
6+
github.com/gorilla/mux v1.8.0
7+
github.com/lingrino/go-fault v1.0.2
8+
)

go.sum

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,12 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
13
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
24
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
5+
github.com/lingrino/go-fault v1.0.2 h1:I7gj2vsxw0wdOwQIX7AZ7kdZRXPX2AgVvRyFXCqSvLA=
6+
github.com/lingrino/go-fault v1.0.2/go.mod h1:+NkrrGRAoJTcF/OCN9Gvj2ctutYWIYQRV4VS9CI7fmo=
7+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
10+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
11+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
12+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/middleware/fault.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package middleware
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"sync"
7+
"time"
8+
9+
"github.com/lingrino/go-fault"
10+
)
11+
12+
type FaultSession struct {
13+
// RequestCount is the number of requests that have been made on this session.
14+
RequestCount int
15+
// Exhausted is set to true when all faults on this session have been exercised.
16+
Exhausted bool
17+
Settings FaultSettings
18+
}
19+
20+
type FaultSettings struct {
21+
// NOTE: The way these fields are ordered represents their precedence in the
22+
// fault chain.
23+
24+
// DelayMS is the number of milliseconds to delay the request.
25+
DelayMS int64 `json:"delay_ms"`
26+
// DelayCount is the number of times to delay the request.
27+
DelayCount int `json:"delay_count"`
28+
29+
// RejectCount is the number of times to reject the request without a response.
30+
// A value greater than 0 enables this fault injector.
31+
RejectCount int `json:"reject_count"`
32+
33+
// ErrorCount is the number of times to return an error status code.
34+
// A value greater than 0 enables this fault injector.
35+
//
36+
// NOTE: Error injection only takes effect after all rejections have
37+
// resolved if both of these injectors are enabled.
38+
ErrorCount int `json:"error_count"`
39+
// ErrorCode is the status code to return when the error injector is enabled.
40+
ErrorCode int `json:"error_code"`
41+
}
42+
43+
func Fault(h http.Handler) http.Handler {
44+
var sessions sync.Map
45+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
46+
reqid := r.Header.Get("request-id")
47+
if reqid == "" {
48+
h.ServeHTTP(w, r)
49+
return
50+
}
51+
52+
settinghdr := r.Header.Get("fault-settings")
53+
if settinghdr == "" {
54+
h.ServeHTTP(w, r)
55+
return
56+
}
57+
58+
session := &FaultSession{}
59+
asession, found := sessions.Load(reqid)
60+
if found {
61+
session = asession.(*FaultSession)
62+
if session.Exhausted {
63+
h.ServeHTTP(w, r)
64+
return
65+
}
66+
}
67+
68+
var settings FaultSettings
69+
err := json.Unmarshal([]byte(settinghdr), &settings)
70+
if err != nil {
71+
http.Error(w, "Invalid fault settings", http.StatusBadRequest)
72+
return
73+
}
74+
75+
reqCount := session.RequestCount
76+
session.Settings = settings
77+
session.RequestCount++
78+
defer func() {
79+
sessions.Store(reqid, session)
80+
}()
81+
82+
var faults []fault.Injector
83+
84+
if settings.DelayMS > 0 && reqCount < settings.DelayCount {
85+
inj, err := fault.NewSlowInjector(time.Millisecond * time.Duration(settings.DelayMS))
86+
if err != nil {
87+
http.Error(w, "Failed to build slow injector", http.StatusInternalServerError)
88+
return
89+
}
90+
91+
faults = append(faults, inj)
92+
}
93+
94+
countOffset := 0
95+
if settings.RejectCount > 0 && reqCount < settings.RejectCount+countOffset {
96+
inj, err := fault.NewRejectInjector()
97+
if err != nil {
98+
http.Error(w, "Failed to build reject injector", http.StatusInternalServerError)
99+
return
100+
}
101+
102+
faults = append(faults, inj)
103+
}
104+
105+
countOffset += settings.RejectCount
106+
if settings.ErrorCode > 0 && reqCount < (settings.ErrorCount+countOffset) {
107+
inj, err := fault.NewErrorInjector(settings.ErrorCode, fault.WithStatusText("Injected error"))
108+
if err != nil {
109+
http.Error(w, "Failed to build error injector", http.StatusInternalServerError)
110+
return
111+
}
112+
113+
faults = append(faults, inj)
114+
}
115+
116+
if len(faults) == 0 {
117+
session.Exhausted = true
118+
h.ServeHTTP(w, r)
119+
return
120+
}
121+
122+
faultchain, err := fault.NewChainInjector(faults)
123+
if err != nil {
124+
http.Error(w, "Failed to build fault chain injector", http.StatusInternalServerError)
125+
}
126+
127+
w.Header().Set("Faults-Enabled", "true")
128+
faultchain.Handler(h).ServeHTTP(w, r)
129+
})
130+
}

0 commit comments

Comments
 (0)