Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion connection/quic_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,11 @@ func (q *quicConnection) handleDataStream(ctx context.Context, stream *rpcquic.R
}

if err, connectResponseSent := q.dispatchRequest(ctx, stream, request); err != nil {
q.logger.Err(err).Str("type", request.Type.String()).Str("dest", request.Dest).Msg("Request failed")
if IsBenignRemoteStreamCancel(err) {
q.logger.Debug().Err(err).Str("type", request.Type.String()).Str("dest", request.Dest).Msg("Request canceled")
} else {
q.logger.Err(err).Str("type", request.Type.String()).Str("dest", request.Dest).Msg("Request failed")
}

// if the connectResponse was already sent and we had an error, we need to propagate it up, so that the stream is
// closed with an RST_STREAM frame
Expand Down
23 changes: 23 additions & 0 deletions connection/stream_cancel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package connection

import (
"errors"

"github.com/quic-go/quic-go"
)

// IsBenignRemoteStreamCancel reports whether err (possibly wrapped) is a remote
// QUIC stream cancellation with error code 0 (NO_ERROR).
//
// Per RFC 9113 §7, HTTP/2 RST_STREAM with NO_ERROR is an intentional,
// non-erroneous stream close. Cloudflare's edge surfaces that to cloudflared as
// a quic.StreamError with Remote=true and ErrorCode=0 (for example when a
// browser tab closes a long-lived SSE or WebSocket). Logging those at ERR
// creates false positives in monitoring; treat them as debug instead.
func IsBenignRemoteStreamCancel(err error) bool {
var se *quic.StreamError
if !errors.As(err, &se) {
return false
}
return se.Remote && se.ErrorCode == 0
}
25 changes: 25 additions & 0 deletions connection/stream_cancel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package connection

import (
"fmt"
"testing"

"github.com/pkg/errors"
"github.com/quic-go/quic-go"
"github.com/stretchr/testify/require"
)

func TestIsBenignRemoteStreamCancel(t *testing.T) {
t.Parallel()

benign := &quic.StreamError{StreamID: 41, ErrorCode: 0, Remote: true}
require.True(t, IsBenignRemoteStreamCancel(benign))
require.True(t, IsBenignRemoteStreamCancel(fmt.Errorf("proxy: %w", benign)))
require.True(t, IsBenignRemoteStreamCancel(errors.Wrap(benign, "proxyHTTPRequest")))

require.False(t, IsBenignRemoteStreamCancel(nil))
require.False(t, IsBenignRemoteStreamCancel(fmt.Errorf("other")))
require.False(t, IsBenignRemoteStreamCancel(&quic.StreamError{StreamID: 41, ErrorCode: 0, Remote: false}))
require.False(t, IsBenignRemoteStreamCancel(&quic.StreamError{StreamID: 41, ErrorCode: 1, Remote: true}))
require.False(t, IsBenignRemoteStreamCancel(&quic.StreamError{StreamID: 41, ErrorCode: 0x100, Remote: true}))
}
6 changes: 6 additions & 0 deletions proxy/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,13 @@ func logOriginHTTPResponse(logger *zerolog.Logger, resp *http.Response) {
}

// logRequestError logs an error for the proxied request.
// Benign remote stream cancellations (QUIC NO_ERROR) are logged at Debug and
// do not increment the request-error metric — see connection.IsBenignRemoteStreamCancel.
func logRequestError(logger *zerolog.Logger, err error) {
if connection.IsBenignRemoteStreamCancel(err) {
logger.Debug().Err(err).Msg("request stream canceled by remote with NO_ERROR")
return
}
requestErrors.Inc()
logger.Error().Err(err).Send()
}
35 changes: 35 additions & 0 deletions proxy/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package proxy

import (
"bytes"
"strings"
"testing"

"github.com/quic-go/quic-go"
"github.com/rs/zerolog"
"github.com/stretchr/testify/require"
)

func TestLogRequestErrorBenignRemoteCancelUsesDebug(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
logger := zerolog.New(&buf).Level(zerolog.DebugLevel)

logRequestError(&logger, &quic.StreamError{StreamID: 41, ErrorCode: 0, Remote: true})
out := buf.String()
require.Contains(t, out, `"level":"debug"`)
require.Contains(t, out, "request stream canceled by remote with NO_ERROR")
require.NotContains(t, out, `"level":"error"`)
}

func TestLogRequestErrorRealErrorUsesError(t *testing.T) {
t.Parallel()

var buf bytes.Buffer
logger := zerolog.New(&buf).Level(zerolog.DebugLevel)

logRequestError(&logger, &quic.StreamError{StreamID: 41, ErrorCode: 1, Remote: true})
out := buf.String()
require.True(t, strings.Contains(out, `"level":"error"`), out)
}