Skip to content

Commit

Permalink
Avoid context canceled errors
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
clwluvw committed Mar 2, 2021
1 parent 245c037 commit d8c05b4
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 3 deletions.
26 changes: 23 additions & 3 deletions middleware/proxy_1_11.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,43 @@
package middleware

import (
"context"
"fmt"
"net/http"
"net/http/httputil"
"strings"

"github.com/labstack/echo/v4"
)

// StatusCodeContextCanceled is a custom HTTP status code for situations
// where a client unexpectedly closed the connection to the server.
// As there is no standard error code for "client closed connection", but
// various well-known HTTP clients and server implement this HTTP code we use
// 499 too instead of the more problematic 5xx, which does not allow to detect this situation
const StatusCodeContextCanceled = 499

func proxyHTTP(tgt *ProxyTarget, c echo.Context, config ProxyConfig) http.Handler {
proxy := httputil.NewSingleHostReverseProxy(tgt.URL)
proxy.ErrorHandler = func(resp http.ResponseWriter, req *http.Request, err error) {
desc := tgt.URL.String()
if tgt.Name != "" {
desc = fmt.Sprintf("%s(%s)", tgt.Name, tgt.URL.String())
}
httpError := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("remote %s unreachable, could not forward: %v", desc, err))
httpError.Internal = err
c.Set("_error", httpError)
// If the client canceled the request (usually by closing the connection), we can report a
// client error (4xx) instead of a server error (5xx) to correctly identify the situation.
// The Go standard library (at of late 2020) wraps the exported, standard
// context.Canceled error with unexported garbage value requiring a substring check, see
// https://github.com/golang/go/blob/6965b01ea248cabb70c3749fd218b36089a21efb/src/net/net.go#L416-L430
if err == context.Canceled || strings.Contains(err.Error(), "operation was canceled") {
httpError := echo.NewHTTPError(StatusCodeContextCanceled, fmt.Sprintf("client closed connection: %v", err))
httpError.Internal = err
c.Set("_error", httpError)
} else {
httpError := echo.NewHTTPError(http.StatusBadGateway, fmt.Sprintf("remote %s unreachable, could not forward: %v", desc, err))
httpError.Internal = err
c.Set("_error", httpError)
}
}
proxy.Transport = config.Transport
proxy.ModifyResponse = config.ModifyResponse
Expand Down
31 changes: 31 additions & 0 deletions middleware/proxy_1_11_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
package middleware

import (
"context"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"

"github.com/labstack/echo/v4"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -50,4 +52,33 @@ func TestProxy_1_11(t *testing.T) {
e.ServeHTTP(rec, req)
assert.Equal(t, "/api/users", req.URL.Path)
assert.Equal(t, http.StatusBadGateway, rec.Code)

// client closed connection
HTTPTarget := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
w.WriteHeader(http.StatusOK)
}))
defer HTTPTarget.Close()
targetURL, _ := url.Parse(HTTPTarget.URL)
target := &ProxyTarget{
Name: "target",
URL: targetURL,
}
rb = NewRandomBalancer(nil)
assert.True(t, rb.AddTarget(target))
e = echo.New()
e.Use(Proxy(rb))
rec = httptest.NewRecorder()
req = httptest.NewRequest(http.MethodGet, "/", nil)
ctx, cancel := context.WithCancel(req.Context())
req = req.WithContext(ctx)
go func() {
startTime := time.Now()
time.Sleep(2 * time.Second)
cancel()
endTime := time.Now()
assert.Less(t, endTime.Sub(startTime).Seconds(), float64(5))
}()
e.ServeHTTP(rec, req)
assert.Equal(t, 499, rec.Code)
}

0 comments on commit d8c05b4

Please sign in to comment.