Skip to content

Commit 1fa6b6e

Browse files
authored
feat: Support connection reset fault injection (#31)
This change supports new `reset_count` fault injection via the `fault-settings` header, which enables connection error testing such as the `x-speakeasy-retries` extension `retryConnectionErrors` configuration. Enabling this injection will immediately close the connection before writing any response (similar to the `reject_count` injection) and sets SO_LINGER to 0, which on most platforms should cause a TCP RST packet.
1 parent b4fda6a commit 1fa6b6e

File tree

2 files changed

+63
-4
lines changed

2 files changed

+63
-4
lines changed

internal/middleware/fault.go

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,23 @@ type FaultSession struct {
1717
Settings FaultSettings
1818
}
1919

20+
// Describes the fault injection settings for a session. The fault chain is
21+
// a series of fault injectors that are applied to the request in order. The
22+
// order of faults is:
23+
// - Delay
24+
// - Reset
25+
// - Reject
26+
// - Error
2027
type FaultSettings struct {
21-
// NOTE: The way these fields are ordered represents their precedence in the
22-
// fault chain.
23-
2428
// DelayMS is the number of milliseconds to delay the request.
2529
DelayMS int64 `json:"delay_ms"`
30+
2631
// DelayCount is the number of times to delay the request.
2732
DelayCount int `json:"delay_count"`
2833

34+
// ResetCount is the number of times to reset the connection.
35+
ResetCount int `json:"reset_count"`
36+
2937
// RejectCount is the number of times to reject the request without a response.
3038
// A value greater than 0 enables this fault injector.
3139
RejectCount int `json:"reject_count"`
@@ -36,6 +44,7 @@ type FaultSettings struct {
3644
// NOTE: Error injection only takes effect after all rejections have
3745
// resolved if both of these injectors are enabled.
3846
ErrorCount int `json:"error_count"`
47+
3948
// ErrorCode is the status code to return when the error injector is enabled.
4049
ErrorCode int `json:"error_code"`
4150
}
@@ -81,6 +90,10 @@ func Fault(h http.Handler) http.Handler {
8190

8291
var faults []fault.Injector
8392

93+
// Since multiple injectors can be enabled, need to count the number of
94+
// requests based on prior injector counts
95+
countOffset := 0
96+
8497
if settings.DelayMS > 0 && reqCount < settings.DelayCount {
8598
inj, err := fault.NewSlowInjector(time.Millisecond * time.Duration(settings.DelayMS))
8699
if err != nil {
@@ -91,7 +104,14 @@ func Fault(h http.Handler) http.Handler {
91104
faults = append(faults, inj)
92105
}
93106

94-
countOffset := 0
107+
// Delay injector does not increase the count offset.
108+
109+
if settings.ResetCount > 0 && reqCount < settings.ResetCount+countOffset {
110+
faults = append(faults, &ConnectionResetInjector{})
111+
}
112+
113+
countOffset += settings.ResetCount
114+
95115
if settings.RejectCount > 0 && reqCount < settings.RejectCount+countOffset {
96116
inj, err := fault.NewRejectInjector()
97117
if err != nil {
@@ -103,6 +123,7 @@ func Fault(h http.Handler) http.Handler {
103123
}
104124

105125
countOffset += settings.RejectCount
126+
106127
if settings.ErrorCode > 0 && reqCount < (settings.ErrorCount+countOffset) {
107128
inj, err := fault.NewErrorInjector(settings.ErrorCode, fault.WithStatusText("Injected error"))
108129
if err != nil {

internal/middleware/reset_injector.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package middleware
2+
3+
import (
4+
"net"
5+
"net/http"
6+
7+
"github.com/lingrino/go-fault"
8+
)
9+
10+
var _ fault.Injector = (*ConnectionResetInjector)(nil)
11+
12+
// Injects a connection error by closing the connection immediately while also
13+
// simulating a TCP RST via setting SO_LINGER to 0.
14+
type ConnectionResetInjector struct{}
15+
16+
func (i *ConnectionResetInjector) Handler(_ http.Handler) http.Handler {
17+
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18+
hijacker, ok := w.(http.Hijacker)
19+
20+
if !ok {
21+
http.Error(w, "connection hijacking not supported", http.StatusInternalServerError)
22+
return
23+
}
24+
25+
conn, _, err := hijacker.Hijack()
26+
27+
if err != nil {
28+
http.Error(w, "failed to hijack connection", http.StatusInternalServerError)
29+
return
30+
}
31+
32+
if tcpConn, ok := conn.(*net.TCPConn); ok {
33+
_ = tcpConn.SetLinger(0) // Best effort RST on close
34+
}
35+
36+
conn.Close()
37+
})
38+
}

0 commit comments

Comments
 (0)