diff --git a/proxyd/backend.go b/proxyd/backend.go index 77d3326b..c84be0a7 100644 --- a/proxyd/backend.go +++ b/proxyd/backend.go @@ -383,6 +383,8 @@ func (b *Backend) Forward(ctx context.Context, reqs []*RPCReq, isBatch bool) ([] res, err := b.doForward(ctx, reqs, isBatch) switch err { case nil: // do nothing + case context.Canceled: + return nil, err case ErrBackendResponseTooLarge: log.Warn( "backend response too large", @@ -583,6 +585,10 @@ func (b *Backend) doForward(ctx context.Context, rpcReqs []*RPCReq, isBatch bool start := time.Now() httpRes, err := b.client.DoLimited(httpReq) if err != nil { + // if it's canceld, we don't want to count it as an error + if errors.Is(err, context.Canceled) { + return nil, err + } b.intermittentErrorsSlidingWindow.Incr() RecordBackendNetworkErrorRateSlidingWindow(b, b.ErrorRate()) return nil, wrapErr(err, "error in backend request") @@ -798,7 +804,12 @@ func (bg *BackendGroup) Forward(ctx context.Context, rpcReqs []*RPCReq, isBatch backendResp := <-ch if backendResp.error != nil { - log.Error("error serving requests", + logfn := log.Error + // If the context was canceled, downgrade the log level to debug. + if errors.Is(backendResp.error, context.Canceled) { + logfn = log.Debug + } + logfn("error serving requests", "req_id", GetReqID(ctx), "auth", GetAuthCtx(ctx), "err", backendResp.error, @@ -1327,6 +1338,9 @@ type LimitedHTTPClient struct { func (c *LimitedHTTPClient) DoLimited(req *http.Request) (*http.Response, error) { if err := c.sem.Acquire(req.Context(), 1); err != nil { + if errors.Is(err, context.Canceled) { + return nil, err + } tooManyRequestErrorsTotal.WithLabelValues(c.backendName).Inc() return nil, wrapErr(err, "too many requests") } @@ -1406,6 +1420,14 @@ func (bg *BackendGroup) ForwardRequestToBackendGroup( if len(rpcReqs) > 0 { res, err = back.Forward(ctx, rpcReqs, isBatch) + if errors.Is(err, context.Canceled) { + log.Info("context canceled", "req_id", GetReqID(ctx), "auth", GetAuthCtx(ctx)) + return &BackendGroupRPCResponse{ + RPCRes: nil, + ServedBy: "", + error: err, + } + } if errors.Is(err, ErrConsensusGetReceiptsCantBeBatched) || errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) || diff --git a/proxyd/backend_test.go b/proxyd/backend_test.go index 73ebebfe..20429925 100644 --- a/proxyd/backend_test.go +++ b/proxyd/backend_test.go @@ -1,9 +1,15 @@ package proxyd import ( + "bytes" + "context" + "encoding/json" + "log/slog" "testing" + "github.com/ethereum/go-ethereum/log" "github.com/stretchr/testify/assert" + "golang.org/x/sync/semaphore" ) func TestStripXFF(t *testing.T) { @@ -20,3 +26,34 @@ func TestStripXFF(t *testing.T) { assert.Equal(t, test.out, actual) } } + +func TestForwardContextCanceled(t *testing.T) { + buf := bytes.NewBuffer(nil) + log.SetDefault(log.NewLogger(slog.NewTextHandler(buf, &slog.HandlerOptions{ + Level: slog.LevelDebug.Level(), + }))) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + sem := semaphore.NewWeighted(100) + backend := NewBackend( + "test", + "http://localhost:8545", + "ws://localhost:8545", + sem, + ) + backendGroup := &BackendGroup{ + Name: "testgroup", + Backends: []*Backend{backend}, + routingStrategy: "fallback", + } + _, _, err := backendGroup.Forward(ctx, []*RPCReq{ + {JSONRPC: "2.0", Method: "eth_blockNumber", ID: json.RawMessage("1")}, + }, true) + assert.ErrorIs(t, context.Canceled, err) + assert.Equalf(t, uint(1), backend.networkRequestsSlidingWindow.Count(), "exact 1 network request should be counted") + assert.Equalf(t, uint(0), backend.intermittentErrorsSlidingWindow.Count(), "no intermittent errors should be counted") + assert.Zerof(t, backend.ErrorRate(), "error rate should be zero") + logs := buf.String() + assert.NotZero(t, logs) + assert.NotContainsf(t, logs, "level=ERROR", "context canceled error should not be logged as a ERROR") +} diff --git a/proxyd/server.go b/proxyd/server.go index 1b529b6d..5932838c 100644 --- a/proxyd/server.go +++ b/proxyd/server.go @@ -532,7 +532,12 @@ func (s *Server) handleBatchRPC(ctx context.Context, reqs []json.RawMessage, isL errors.Is(err, ErrConsensusGetReceiptsInvalidTarget) { return nil, false, "", err } - log.Error( + logfn := log.Error + // If the context was canceled, downgrade the log level to debug. + if errors.Is(err, context.Canceled) { + logfn = log.Debug + } + logfn( "error forwarding RPC batch", "batch_size", len(elems), "backend_group", group,