Skip to content

fix(triple): preserve CodeBizError over the Triple unary transport - #3655

Merged
Alanxtl merged 1 commit into
apache:developfrom
codeAnqiang-ma:fix/triple-preserve-bizerror-code
Aug 16, 2026
Merged

Alanxtl merged 1 commit into
apache:developfrom
codeAnqiang-ma:fix/triple-preserve-bizerror-code

Conversation

@codeAnqiang-ma

@codeAnqiang-ma codeAnqiang-ma commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes #3654

Over the Triple (non-gRPC) unary transport, a server-returned CodeBizError (17) reaches the client as CodeUnknown (2); the gRPC transport in the same package preserves it. Downstream, failover.isBizError stops recognizing business errors and retries them (default up to 3 attempts, so non-idempotent calls run repeatedly), and metrics/rpc counts them as unknown.

Root cause. tripleWireError.asError() (protocol_triple.go:694) clamps any code outside [minCode, maxCode] = [1,16] to CodeUnknown. That clamp comes from connect-go, which only defines codes 1..16; CodeBizError = 17 is a dubbo-go extension and maxCode was never updated. code.go deliberately supports such codes — the UnmarshalText branch commented "Ensure that non-canonical codes round-trip through MarshalText and UnmarshalText" keeps out-of-range codes for exactly this reason. asError() then discards what UnmarshalText just preserved.

Fix. Exempt CodeBizError from the clamp (one line), plus TestBizErrorCodePreservedAcrossProtocols: a real httptest server whose handler returns NewError(CodeBizError, ...), asserting the client-visible code over both transports. Before the fix the triple subtest fails (got: unknown, want: code_17) while grpc passes; after, both pass.

Why not raise maxCode to CodeBizError instead

That breaks the round-trip. The UnmarshalText fallback only keeps codes outside [minCode, maxCode]:

if err == nil && (code < uint64(minCode) || code > uint64(maxCode)) {
	*c = Code(code)
	return nil
}

With maxCode = CodeBizError, the string code_17 no longer satisfies that condition and falls through to invalid code %q. Making it work needs a canonical string for CodeBizError in both String() and UnmarshalText, which changes the wire representation of the code. The clamp exemption keeps the existing wire format and touches one line.

Test output (before / after)

Before the fix — only protocol_triple.go reverted, test unchanged:

$ go test ./protocol/triple/triple_protocol/ -run TestBizErrorCodePreservedAcrossProtocols -v
=== NAME  TestBizErrorCodePreservedAcrossProtocols/triple
    triple_ext_test.go:772:
        assertion:	assert.Equal
        got:	unknown
        want:	code_17
--- FAIL: TestBizErrorCodePreservedAcrossProtocols (0.00s)
    --- FAIL: TestBizErrorCodePreservedAcrossProtocols/triple (0.01s)
    --- PASS: TestBizErrorCodePreservedAcrossProtocols/grpc (0.01s)
FAIL	dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol	4.304s

After:

--- PASS: TestBizErrorCodePreservedAcrossProtocols (0.00s)
    --- PASS: TestBizErrorCodePreservedAcrossProtocols/triple (0.00s)
    --- PASS: TestBizErrorCodePreservedAcrossProtocols/grpc (0.02s)
ok  	dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol	0.970s

Existing tests, including the packages that consume the code:

$ go test ./protocol/triple/... ./cluster/cluster/failover/... ./metrics/rpc/...
ok  	dubbo.apache.org/dubbo-go/v3/cluster/cluster/failover	1.675s
ok  	dubbo.apache.org/dubbo-go/v3/metrics/rpc	2.234s
ok  	dubbo.apache.org/dubbo-go/v3/protocol/triple	5.790s
ok  	dubbo.apache.org/dubbo-go/v3/protocol/triple/health	1.687s
ok  	dubbo.apache.org/dubbo-go/v3/protocol/triple/openapi	2.138s
ok  	dubbo.apache.org/dubbo-go/v3/protocol/triple/reflection	2.667s
ok  	dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol	18.123s

go vet ./protocol/triple/triple_protocol/... and gofmt -l are both clean.

Not run locally: the full make test and the integration suite (they need ZooKeeper / Nacos / etcd / Docker, which I don't have set up), and golangci-lint. Leaving those to CI.

Checklist

  • I confirm the target branch is develop
  • Code has passed local testing
  • I have added tests that prove my fix is effective or that my feature works

Assisted-by: Cursor (Claude Opus 5). Reproduced, reviewed and tested locally by the author.

Over the Triple (non-gRPC) unary transport, tripleWireError.asError()
clamped any code outside [minCode, maxCode] to CodeUnknown, so a
server-returned CodeBizError (17) reached the client as CodeUnknown (2).
The clamp comes from connect-go, which only defines codes 1..16;
CodeBizError is a dubbo-go extension and maxCode was never updated.
code.go already goes out of its way to keep such codes intact, so that
UnmarshalText round-trips them, and asError then discarded them again.

As a result failover.isBizError stopped recognizing business errors and
retried them, and metrics/rpc counted them as unknown failures. The gRPC
transport was unaffected because grpcErrorFromTrailer does not clamp.

Exempt CodeBizError from the clamp and add a regression test asserting
the client-visible code over both transports.

Fixes: apache#3654

Assisted-by: Cursor (Claude Opus 5)
Signed-off-by: codeAnqiang-ma <273298913+codeAnqiang-ma@users.noreply.github.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@sonarqubecloud

Copy link
Copy Markdown

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 54.57%. Comparing base (60d1c2a) to head (0cb00c7).
⚠️ Report is 917 commits behind head on develop.

Files with missing lines Patch % Lines
protocol/triple/triple_protocol/protocol_triple.go 0.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #3655      +/-   ##
===========================================
+ Coverage    46.76%   54.57%   +7.81%     
===========================================
  Files          295      474     +179     
  Lines        17172    36701   +19529     
===========================================
+ Hits          8031    20031   +12000     
- Misses        8287    15050    +6763     
- Partials       854     1620     +766     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a Triple (non-gRPC) unary transport decoding issue where a server-returned CodeBizError (17) was being clamped to CodeUnknown, aligning behavior with the gRPC transport and preventing downstream retry/metrics misclassification.

Changes:

  • Preserve CodeBizError in tripleWireError.asError() by exempting it from the out-of-range clamp.
  • Add a regression test that spins up real httptest servers and asserts the client-visible code is preserved over both Triple and gRPC unary transports.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
protocol/triple/triple_protocol/protocol_triple.go Exempts CodeBizError from the wire-code clamp during error reconstruction.
protocol/triple/triple_protocol/triple_ext_test.go Adds an end-to-end regression test verifying CodeBizError survives across both transports.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +723 to +732
assertBizError := func(t *testing.T, server *httptest.Server, opts ...triple.ClientOption) { //nolint:thelper
client := pingv1connect.NewPingServiceClient(server.Client(), server.URL, opts...)
request := triple.NewRequest(&pingv1.PingRequest{Number: 42})
response := triple.NewResponse(&pingv1.PingResponse{})
err := client.Ping(context.Background(), request, response)
assert.NotNil(t, err)
var tripleErr *triple.Error
assert.True(t, errors.As(err, &tripleErr))
assert.Equal(t, tripleErr.Code(), triple.CodeBizError)
}
@Alanxtl

Alanxtl commented Aug 14, 2026

Copy link
Copy Markdown
Member

cc @CAICAIIs , help review this please

@Alanxtl Alanxtl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@Alanxtl
Alanxtl merged commit e74a8d7 into apache:develop Aug 16, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

3.3.3 version 3.3.3 ☢️ Bug

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Triple unary transport rewrites CodeBizError(17) to CodeUnknown on the client

5 participants