Skip to content

Commit 591dc97

Browse files
committed
Avoid context canceled errors
Return 499 Client Closed Request when the client has closed the request before the server could send a response Signed-off-by: Seena Fallah <seenafallah@gmail.com>
1 parent 54ac0da commit 591dc97

File tree

2 files changed

+52
-3
lines changed

2 files changed

+52
-3
lines changed

middleware/proxy_1_11.go

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,44 @@
33
package middleware
44

55
import (
6+
"context"
67
"fmt"
78
"net/http"
89
"net/http/httputil"
10+
"strings"
911

1012
"github.com/labstack/echo/v4"
1113
)
1214

15+
// StatusCodeContextCanceled is HTTP status code for when client closed connection
16+
// regrettably, there is no standard error code for "client closed connection", but
17+
// for historical reasons we can use a code that a lot of people are already using;
18+
// using 5xx is problematic for users;
19+
const StatusCodeContextCanceled = 499
20+
1321
func proxyHTTP(tgt *ProxyTarget, c echo.Context, config ProxyConfig) http.Handler {
1422
proxy := httputil.NewSingleHostReverseProxy(tgt.URL)
1523
proxy.ErrorHandler = func(resp http.ResponseWriter, req *http.Request, err error) {
1624
desc := tgt.URL.String()
1725
if tgt.Name != "" {
1826
desc = fmt.Sprintf("%s(%s)", tgt.Name, tgt.URL.String())
1927
}
20-
httpError := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("remote %s unreachable, could not forward: %v", desc, err))
21-
httpError.Internal = err
22-
c.Set("_error", httpError)
28+
// if the client canceled the request (usually this means they closed
29+
// the connection, so they won't see any response), we can report it
30+
// as a client error (4xx) and not a server error (5xx); unfortunately
31+
// the Go standard library, at least at time of writing in late 2020,
32+
// obnoxiously wraps the exported, standard context.Canceled error with
33+
// an unexported garbage value that we have to do a substring check for:
34+
// https://github.com/golang/go/blob/6965b01ea248cabb70c3749fd218b36089a21efb/src/net/net.go#L416-L430
35+
if err == context.Canceled || strings.Contains(err.Error(), "operation was canceled") {
36+
httpError := echo.NewHTTPError(StatusCodeContextCanceled, fmt.Sprintf("client closed connection: %v", err))
37+
httpError.Internal = err
38+
c.Set("_error", httpError)
39+
} else {
40+
httpError := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("remote %s unreachable, could not forward: %v", desc, err))
41+
httpError.Internal = err
42+
c.Set("_error", httpError)
43+
}
2344
}
2445
proxy.Transport = config.Transport
2546
proxy.ModifyResponse = config.ModifyResponse

middleware/proxy_1_11_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import (
77
"net/http/httptest"
88
"net/url"
99
"testing"
10+
"time"
1011

1112
"github.com/labstack/echo/v4"
1213
"github.com/stretchr/testify/assert"
14+
"golang.org/x/net/context"
1315
)
1416

1517
func TestProxy_1_11(t *testing.T) {
@@ -50,4 +52,30 @@ func TestProxy_1_11(t *testing.T) {
5052
e.ServeHTTP(rec, req)
5153
assert.Equal(t, "/api/users", req.URL.Path)
5254
assert.Equal(t, http.StatusBadGateway, rec.Code)
55+
56+
// client closed connection
57+
HTTPTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
58+
time.Sleep(30 * time.Second)
59+
w.WriteHeader(http.StatusOK)
60+
}))
61+
defer HTTPTarget.Close()
62+
targetURL, _ := url.Parse(HTTPTarget.URL)
63+
target := &ProxyTarget{
64+
Name: "target",
65+
URL: targetURL,
66+
}
67+
rb = NewRandomBalancer(nil)
68+
assert.True(t, rb.AddTarget(target))
69+
e = echo.New()
70+
e.Use(Proxy(rb))
71+
rec = httptest.NewRecorder()
72+
req = httptest.NewRequest(http.MethodGet, "/", nil)
73+
ctx, cancel := context.WithCancel(req.Context())
74+
req = req.WithContext(ctx)
75+
e.ServeHTTP(rec, req)
76+
startTime := time.Now()
77+
cancel()
78+
endTime := time.Now()
79+
assert.Less(t, endTime.Sub(startTime).Seconds(), float64(30))
80+
assert.Equal(t, 499, rec.Code)
5381
}

0 commit comments

Comments
 (0)