Skip to content

Commit 4d6bd6b

Browse files
committed
fix: map proxy errors to correct HTTP status codes
Previously, the apiserver-network-proxy returned a blanket 503 (Service Unavailable) status for all backend connection failures, which was incorrect for a proxy server. Additionally, HTTP CONNECT mode would send 200 OK immediately after hijacking, preventing proper error reporting but also violating HTTP CONNECT protocol This commit: - Adds mapDialErrorToHTTPStatus() to map TCP/network errors to appropriate HTTP status codes: * 502 Bad Gateway: connection refused, DNS failures, network unreachable * 503 Service Unavailable: resource exhaustion (too many open files) * 504 Gateway Timeout: I/O timeouts, deadline exceeded - Delays sending "200 Connection Established" until after successful backend connection in HTTP CONNECT mode - Ensures proper HTTP/1.1 protocol version in error responses - Preserves original error messages in response body for debugging Signed-off-by: Imran Pochi <[email protected]>
1 parent 3737fb2 commit 4d6bd6b

File tree

2 files changed

+76
-12
lines changed

2 files changed

+76
-12
lines changed

pkg/server/server.go

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,53 @@ const (
108108
destHostKey key = iota
109109
)
110110

111+
// mapDialErrorToHTTPStatus maps common TCP/network error strings to appropriate HTTP status codes
112+
func mapDialErrorToHTTPStatus(errStr string) int {
113+
// Convert to lowercase for case-insensitive matching
114+
errLower := strings.ToLower(errStr)
115+
116+
// Check each error pattern and return appropriate status code
117+
switch {
118+
// Timeouts - backend didn't respond in time -> 504 Gateway Timeout
119+
case strings.Contains(errLower, "i/o timeout"),
120+
strings.Contains(errLower, "deadline exceeded"),
121+
strings.Contains(errLower, "context deadline exceeded"),
122+
strings.Contains(errLower, "timeout"):
123+
return 504
124+
125+
// Resource exhaustion errors -> 503 Service Unavailable
126+
case strings.Contains(errLower, "too many open files"),
127+
strings.Contains(errLower, "socket: too many open files"):
128+
return 503
129+
130+
// Connection errors -> 502 Bad Gateway
131+
case strings.Contains(errLower, "connection refused"),
132+
strings.Contains(errLower, "connection reset by peer"),
133+
strings.Contains(errLower, "broken pipe"),
134+
strings.Contains(errLower, "network is unreachable"),
135+
strings.Contains(errLower, "no route to host"),
136+
strings.Contains(errLower, "host is unreachable"),
137+
strings.Contains(errLower, "network is down"):
138+
return 502
139+
140+
// DNS resolution failures -> 502 Bad Gateway
141+
case strings.Contains(errLower, "no such host"),
142+
strings.Contains(errLower, "name resolution"),
143+
strings.Contains(errLower, "lookup") && strings.Contains(errLower, "no such host"):
144+
return 502
145+
146+
// TLS/SSL errors -> 502 Bad Gateway
147+
case strings.Contains(errLower, "tls"),
148+
strings.Contains(errLower, "ssl"),
149+
strings.Contains(errLower, "certificate"):
150+
return 502
151+
152+
// Default to 502 Bad Gateway for unknown proxy errors
153+
default:
154+
return 502
155+
}
156+
}
157+
111158
func (c *ProxyClientConnection) send(pkt *client.Packet) error {
112159
defer func(start time.Time) { metrics.Metrics.ObserveFrontendWriteLatency(time.Since(start)) }(time.Now())
113160
if c.Mode == ModeGRPC {
@@ -122,11 +169,21 @@ func (c *ProxyClientConnection) send(pkt *client.Packet) error {
122169
_, err := c.HTTP.Write(pkt.GetData().Data)
123170
return err
124171
} else if pkt.Type == client.PacketType_DIAL_RSP {
125-
if pkt.GetDialResponse().Error != "" {
126-
body := bytes.NewBufferString(pkt.GetDialResponse().Error)
172+
dialErr := pkt.GetDialResponse().Error
173+
if dialErr != "" {
174+
// // Map the error to appropriate HTTP status code
175+
statusCode := mapDialErrorToHTTPStatus(dialErr)
176+
statusText := http.StatusText(statusCode)
177+
body := bytes.NewBufferString(dialErr)
127178
t := http.Response{
128-
StatusCode: 503,
179+
StatusCode: statusCode,
180+
Status: fmt.Sprintf("%d %s", statusCode, statusText),
129181
Body: io.NopCloser(body),
182+
Header: http.Header{
183+
"Content-Type": []string{"text/plain; charset=utf-8"},
184+
},
185+
Proto: "HTTP/1.1",
186+
ProtoMinor: 1,
130187
}
131188

132189
t.Write(c.HTTP)

pkg/server/tunnel.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,6 @@ func (t *Tunnel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
6060
return
6161
}
6262

63-
// Send the HTTP 200 OK status after a successful hijack
64-
_, err = conn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
65-
if err != nil {
66-
klog.ErrorS(err, "failed to send 200 connection established")
67-
conn.Close()
68-
return
69-
}
70-
7163
var closeOnce sync.Once
7264
defer closeOnce.Do(func() { conn.Close() })
7365

@@ -110,11 +102,17 @@ func (t *Tunnel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
110102
t.Server.PendingDial.Add(random, connection)
111103
if err := backend.Send(dialRequest); err != nil {
112104
klog.ErrorS(err, "failed to tunnel dial request", "host", r.Host, "dialID", connection.dialID, "agentID", connection.agentID)
105+
// Send proper HTTP error response
106+
conn.Write([]byte(fmt.Sprintf("HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nFailed to tunnel dial request: %v\r\n", err)))
107+
conn.Close()
113108
return
114109
}
115110
ctxt := backend.Context()
116111
if ctxt.Err() != nil {
117-
klog.ErrorS(err, "context reports failure")
112+
klog.ErrorS(ctxt.Err(), "context reports failure")
113+
conn.Write([]byte(fmt.Sprintf("HTTP/1.1 502 Bad Gateway\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nBackend context error: %v\r\n", ctxt.Err())))
114+
conn.Close()
115+
return
118116
}
119117

120118
select {
@@ -125,6 +123,15 @@ func (t *Tunnel) ServeHTTP(w http.ResponseWriter, r *http.Request) {
125123

126124
select {
127125
case <-connection.connected: // Waiting for response before we begin full communication.
126+
// Now that connection is established, send 200 OK to switch to tunnel mode
127+
_, err = conn.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n"))
128+
if err != nil {
129+
klog.ErrorS(err, "failed to send 200 connection established", "host", r.Host, "agentID", connection.agentID)
130+
conn.Close()
131+
return
132+
}
133+
klog.V(3).InfoS("Connection established, sent 200 OK", "host", r.Host, "agentID", connection.agentID, "connectionID", connection.connectID)
134+
128135
case <-closed: // Connection was closed before being established
129136
}
130137

0 commit comments

Comments
 (0)