You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by xu...@apache.org on 2026/03/25 07:51:48 UTC

(dubbo-go) branch main updated (2028c37a4 -> fb8eff2f0)

This is an automated email from the ASF dual-hosted git repository.

xuetaoli pushed a change to branch main
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git


 discard 2028c37a4 fix(triple): case-insensitive method routing without metadata polluti… (#3277)
     add 5c6bee720 refactor: migrate hystrix filter from main repository (#3253)
     add 8fe1f4bc6 fix(router): resolve polaris application lookup from URL attributes (#3256)
     new fb8eff2f0 fix(triple): case-insensitive method routing without metadata polluti… (#3277)

This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version.  This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:

 * -- * -- B -- O -- O -- O   (2028c37a4)
            \
             N -- N -- N   refs/heads/main (fb8eff2f0)

You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.

Any revisions marked "omit" are not gone; other references still
refer to them.  Any revisions marked "discard" are gone forever.

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 cluster/router/polaris/router.go                   |  45 ++-
 cluster/router/polaris/router_test.go              | 112 +++++++
 common/constant/key.go                             |   2 -
 common/extension/filter.go                         |   2 +-
 filter/README.md                                   |   4 +-
 filter/filter_impl/import.go                       |   1 -
 filter/hystrix/filter.go                           | 335 ---------------------
 filter/hystrix/filter_test.go                      | 246 ---------------
 go.mod                                             |   1 -
 go.sum                                             |   1 -
 imports/imports.go                                 |   1 -
 server/options.go                                  |   8 -
 server/options_test.go                             |   9 -
 .../cmd/testGenCode/template/newApp/go.mod         |   1 -
 .../cmd/testGenCode/template/newDemo/go.mod        |   1 -
 tools/dubbogo-cli/generator/application/gomod.go   |   1 -
 tools/dubbogo-cli/generator/sample/mod.go          |   1 -
 17 files changed, 152 insertions(+), 619 deletions(-)
 create mode 100644 cluster/router/polaris/router_test.go
 delete mode 100644 filter/hystrix/filter.go
 delete mode 100644 filter/hystrix/filter_test.go


(dubbo-go) 01/01: fix(triple): case-insensitive method routing without metadata polluti… (#3277)

Posted by xu...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

xuetaoli pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/dubbo-go.git

commit fb8eff2f090851261e2d1acfc55414ede91ca9c0
Author: 承潜 <29...@qq.com>
AuthorDate: Wed Mar 25 10:49:35 2026 +0800

    fix(triple): case-insensitive method routing without metadata polluti… (#3277)
    
    * fix(triple): case-insensitive method routing without metadata pollution (#3162)
    
    Introduce methodRouteMux in the Triple transport layer to support
    case-insensitive method name routing (Go's GetUser <-> Java's getUser).
    
    Remove the SwapCase method duplication from enhanceServiceInfo, which
    was polluting ServiceInfo.Methods with duplicate entries visible to
    registry and gRPC reflection.
    
    * style(triple): fix test import formatting
    
    * fix(ci): address triple sonar warnings
    
    * style(triple): modernize attachment map type
    
    * test(triple): split method handler tests and fix lint assertion
    
    * fix(triple): stabilize method fallback and host-unavailable tests
    
    * test(triple): extract repeated streaming literals
    
    * test(triple): make lowerFirstRune empty-input path coverable
---
 protocol/triple/server.go                          | 221 +++++----
 protocol/triple/server_test.go                     | 527 +++++++++++++++++++++
 .../triple/triple_protocol/method_route_mux.go     | 118 +++++
 .../triple_protocol/method_route_mux_test.go       | 208 ++++++++
 protocol/triple/triple_protocol/server.go          |   4 +-
 protocol/triple/triple_protocol/triple_ext_test.go |  12 +-
 server/server.go                                   |  34 +-
 server/server_test.go                              |  55 ++-
 server/triple_case_route_integration_test.go       | 255 ++++++++++
 9 files changed, 1304 insertions(+), 130 deletions(-)

diff --git a/protocol/triple/server.go b/protocol/triple/server.go
index 244377b63..7c46ac7fc 100644
--- a/protocol/triple/server.go
+++ b/protocol/triple/server.go
@@ -320,108 +320,133 @@ func (s *Server) compatRegisterHandler(interfaceName string, svc dubbo3.Dubbo3Gr
 	}
 }
 
-// handleServiceWithInfo injects invoker and create handler based on ServiceInfo
+// handleServiceWithInfo injects invoker and creates handlers based on ServiceInfo.
+// Each method is registered once under its canonical procedure path. Triple's
+// transport-layer route mux performs case-insensitive fallback matching.
 func (s *Server) handleServiceWithInfo(interfaceName string, invoker base.Invoker, info *common.ServiceInfo, opts ...tri.HandlerOption) {
 	for _, method := range info.Methods {
 		m := method
 		procedure := joinProcedure(interfaceName, method.Name)
-		switch m.Type {
-		case constant.CallUnary:
-			_ = s.triServer.RegisterUnaryHandler(
-				procedure,
-				m.ReqInitFunc,
-				func(ctx context.Context, req *tri.Request) (*tri.Response, error) {
-					var args []any
-					if argsRaw, ok := req.Msg.([]any); ok {
-						// non-idl mode, req.Msg consists of many arguments
-						for _, argRaw := range argsRaw {
-							// refer to createServiceInfoWithReflection, in ReqInitFunc, argRaw is a pointer to real arg.
-							// so we have to invoke Elem to get the real arg.
-							args = append(args, reflect.ValueOf(argRaw).Elem().Interface())
-						}
-					} else {
-						// triple idl mode and old triple idl mode
-						args = append(args, req.Msg)
-					}
-					attachments := generateAttachments(req.Header())
-					// inject attachments
-					ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
-					invo := invocation.NewRPCInvocation(m.Name, args, attachments)
-					res := invoker.Invoke(ctx, invo)
-					// todo(DMwangnima): modify InfoInvoker to get a unified processing logic
-					// please refer to server/InfoInvoker.Invoke()
-					var triResp *tri.Response
-					if existingResp, ok := res.Result().(*tri.Response); ok {
-						triResp = existingResp
-					} else {
-						// please refer to proxy/proxy_factory/ProxyInvoker.Invoke
-						triResp = tri.NewResponse([]any{res.Result()})
-					}
-					for k, v := range res.Attachments() {
-						switch val := v.(type) {
-						case string:
-							tri.AppendToOutgoingContext(ctx, k, val)
-						case []string:
-							for _, v := range val {
-								tri.AppendToOutgoingContext(ctx, k, v)
-							}
-						}
-					}
-					return triResp, res.Error()
-				},
-				opts...,
-			)
-		case constant.CallClientStream:
-			_ = s.triServer.RegisterClientStreamHandler(
-				procedure,
-				func(ctx context.Context, stream *tri.ClientStream) (*tri.Response, error) {
-					var args []any
-					args = append(args, m.StreamInitFunc(stream))
-					attachments := generateAttachments(stream.RequestHeader())
-					// inject attachments
-					ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
-					invo := invocation.NewRPCInvocation(m.Name, args, attachments)
-					res := invoker.Invoke(ctx, invo)
-					if triResp, ok := res.Result().(*tri.Response); ok {
-						return triResp, res.Error()
-					}
-					// please refer to proxy/proxy_factory/ProxyInvoker.Invoke
-					triResp := tri.NewResponse([]any{res.Result()})
-					return triResp, res.Error()
-				},
-				opts...,
-			)
-		case constant.CallServerStream:
-			_ = s.triServer.RegisterServerStreamHandler(
-				procedure,
-				m.ReqInitFunc,
-				func(ctx context.Context, req *tri.Request, stream *tri.ServerStream) error {
-					var args []any
-					args = append(args, req.Msg, m.StreamInitFunc(stream))
-					attachments := generateAttachments(req.Header())
-					// inject attachments
-					ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
-					invo := invocation.NewRPCInvocation(m.Name, args, attachments)
-					res := invoker.Invoke(ctx, invo)
-					return res.Error()
-				},
-				opts...,
-			)
-		case constant.CallBidiStream:
-			_ = s.triServer.RegisterBidiStreamHandler(
-				procedure,
-				func(ctx context.Context, stream *tri.BidiStream) error {
-					var args []any
-					args = append(args, m.StreamInitFunc(stream))
-					attachments := generateAttachments(stream.RequestHeader())
-					// inject attachments
-					ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
-					invo := invocation.NewRPCInvocation(m.Name, args, attachments)
-					res := invoker.Invoke(ctx, invo)
-					return res.Error()
-				},
-				opts...,
-			)
+		s.registerMethodHandler(procedure, m, invoker, opts...)
+	}
+}
+
+// registerMethodHandler registers a single method handler for the given procedure path.
+func (s *Server) registerMethodHandler(procedure string, m common.MethodInfo, invoker base.Invoker, opts ...tri.HandlerOption) {
+	switch m.Type {
+	case constant.CallUnary:
+		s.registerUnaryMethodHandler(procedure, m, invoker, opts...)
+	case constant.CallClientStream:
+		s.registerClientStreamMethodHandler(procedure, m, invoker, opts...)
+	case constant.CallServerStream:
+		s.registerServerStreamMethodHandler(procedure, m, invoker, opts...)
+	case constant.CallBidiStream:
+		s.registerBidiStreamMethodHandler(procedure, m, invoker, opts...)
+	}
+}
+
+func (s *Server) registerUnaryMethodHandler(procedure string, m common.MethodInfo, invoker base.Invoker, opts ...tri.HandlerOption) {
+	_ = s.triServer.RegisterUnaryHandler(
+		procedure,
+		m.ReqInitFunc,
+		func(ctx context.Context, req *tri.Request) (*tri.Response, error) {
+			args := extractUnaryInvocationArgs(req.Msg)
+			attachments := generateAttachments(req.Header())
+			// inject attachments
+			ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
+			invo := invocation.NewRPCInvocation(m.Name, args, attachments)
+			res := invoker.Invoke(ctx, invo)
+			// todo(DMwangnima): modify InfoInvoker to get a unified processing logic
+			// please refer to server/InfoInvoker.Invoke()
+			triResp := wrapTripleResponse(res.Result())
+			appendTripleOutgoingAttachments(ctx, res.Attachments())
+			return triResp, res.Error()
+		},
+		opts...,
+	)
+}
+
+func (s *Server) registerClientStreamMethodHandler(procedure string, m common.MethodInfo, invoker base.Invoker, opts ...tri.HandlerOption) {
+	_ = s.triServer.RegisterClientStreamHandler(
+		procedure,
+		func(ctx context.Context, stream *tri.ClientStream) (*tri.Response, error) {
+			args := []any{m.StreamInitFunc(stream)}
+			attachments := generateAttachments(stream.RequestHeader())
+			// inject attachments
+			ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
+			invo := invocation.NewRPCInvocation(m.Name, args, attachments)
+			res := invoker.Invoke(ctx, invo)
+			return wrapTripleResponse(res.Result()), res.Error()
+		},
+		opts...,
+	)
+}
+
+func (s *Server) registerServerStreamMethodHandler(procedure string, m common.MethodInfo, invoker base.Invoker, opts ...tri.HandlerOption) {
+	_ = s.triServer.RegisterServerStreamHandler(
+		procedure,
+		m.ReqInitFunc,
+		func(ctx context.Context, req *tri.Request, stream *tri.ServerStream) error {
+			args := []any{req.Msg, m.StreamInitFunc(stream)}
+			attachments := generateAttachments(req.Header())
+			// inject attachments
+			ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
+			invo := invocation.NewRPCInvocation(m.Name, args, attachments)
+			res := invoker.Invoke(ctx, invo)
+			return res.Error()
+		},
+		opts...,
+	)
+}
+
+func (s *Server) registerBidiStreamMethodHandler(procedure string, m common.MethodInfo, invoker base.Invoker, opts ...tri.HandlerOption) {
+	_ = s.triServer.RegisterBidiStreamHandler(
+		procedure,
+		func(ctx context.Context, stream *tri.BidiStream) error {
+			args := []any{m.StreamInitFunc(stream)}
+			attachments := generateAttachments(stream.RequestHeader())
+			// inject attachments
+			ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
+			invo := invocation.NewRPCInvocation(m.Name, args, attachments)
+			res := invoker.Invoke(ctx, invo)
+			return res.Error()
+		},
+		opts...,
+	)
+}
+
+func extractUnaryInvocationArgs(msg any) []any {
+	if argsRaw, ok := msg.([]any); ok {
+		args := make([]any, 0, len(argsRaw))
+		// non-idl mode, req.Msg consists of many arguments
+		for _, argRaw := range argsRaw {
+			// refer to createServiceInfoWithReflection, in ReqInitFunc, argRaw is a pointer to real arg.
+			// so we have to invoke Elem to get the real arg.
+			args = append(args, reflect.ValueOf(argRaw).Elem().Interface())
+		}
+		return args
+	}
+	// triple idl mode and old triple idl mode
+	return []any{msg}
+}
+
+func wrapTripleResponse(result any) *tri.Response {
+	if existingResp, ok := result.(*tri.Response); ok {
+		return existingResp
+	}
+	// please refer to proxy/proxy_factory/ProxyInvoker.Invoke
+	return tri.NewResponse([]any{result})
+}
+
+func appendTripleOutgoingAttachments(ctx context.Context, attachments map[string]any) {
+	for k, v := range attachments {
+		switch val := v.(type) {
+		case string:
+			tri.AppendToOutgoingContext(ctx, k, val)
+		case []string:
+			for _, item := range val {
+				tri.AppendToOutgoingContext(ctx, k, item)
+			}
 		}
 	}
 }
diff --git a/protocol/triple/server_test.go b/protocol/triple/server_test.go
index f6d801954..99da82179 100644
--- a/protocol/triple/server_test.go
+++ b/protocol/triple/server_test.go
@@ -21,6 +21,7 @@ import (
 	"context"
 	"fmt"
 	"net/http"
+	"net/http/httptest"
 	"reflect"
 	"sync"
 	"testing"
@@ -29,6 +30,7 @@ import (
 
 import (
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 
 	"google.golang.org/grpc"
 )
@@ -37,6 +39,9 @@ import (
 	"dubbo.apache.org/dubbo-go/v3/common"
 	"dubbo.apache.org/dubbo-go/v3/common/constant"
 	"dubbo.apache.org/dubbo-go/v3/global"
+	"dubbo.apache.org/dubbo-go/v3/protocol/base"
+	"dubbo.apache.org/dubbo-go/v3/protocol/result"
+	tri "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
 )
 
 func Test_generateAttachments(t *testing.T) {
@@ -542,3 +547,525 @@ func Test_isReflectValueNil_UnsafePointer(t *testing.T) {
 		assert.False(t, isReflectValueNil(v))
 	})
 }
+
+// TestHandleServiceWithInfoSaveServiceInfoOnlyOriginalMethods verifies that
+// saveServiceInfo only records the original method names so registry and
+// gRPC-reflection metadata stay clean.
+func TestHandleServiceWithInfoSaveServiceInfoOnlyOriginalMethods(t *testing.T) {
+	server := NewServer(nil)
+	info := &common.ServiceInfo{
+		Methods: []common.MethodInfo{
+			{Name: "GetUser", Type: constant.CallUnary},
+			{Name: "ListUsers", Type: constant.CallServerStream},
+		},
+	}
+	server.saveServiceInfo("com.example.UserService", info)
+
+	svcInfo := server.GetServiceInfo()
+	svc, ok := svcInfo["com.example.UserService"]
+	assert.True(t, ok)
+	// Only the two original methods; no "getUser" / "listUsers" aliases.
+	assert.Len(t, svc.Methods, 2)
+	names := make([]string, 0, len(svc.Methods))
+	for _, m := range svc.Methods {
+		names = append(names, m.Name)
+	}
+	assert.Contains(t, names, "GetUser")
+	assert.Contains(t, names, "ListUsers")
+	assert.NotContains(t, names, "getUser")
+	assert.NotContains(t, names, "listUsers")
+}
+
+type tripleServerTestInvoker struct {
+	invokeFn func(context.Context, base.Invocation) result.Result
+}
+
+func (m *tripleServerTestInvoker) GetURL() *common.URL {
+	return common.NewURLWithOptions()
+}
+
+func (m *tripleServerTestInvoker) IsAvailable() bool {
+	return true
+}
+
+func (m *tripleServerTestInvoker) Destroy() {
+	// No-op: this test double does not own lifecycle resources.
+}
+
+func (m *tripleServerTestInvoker) Invoke(ctx context.Context, invocation base.Invocation) result.Result {
+	if m.invokeFn == nil {
+		return &result.RPCResult{}
+	}
+	return m.invokeFn(ctx, invocation)
+}
+
+type tripleServerTestConn struct {
+	reqHeader   http.Header
+	respHeader  http.Header
+	respTrailer http.Header
+	receiveFn   func(any)
+	sent        []any
+}
+
+const (
+	tripleServerTestFromClientMessage = "from-client"
+	tripleServerTestServerToken       = "server-token"
+	tripleServerTestBidiToken         = "bidi-token"
+	tripleServerTestBidiValue         = "bidi-v"
+)
+
+func newTripleServerTestConn() *tripleServerTestConn {
+	return &tripleServerTestConn{
+		reqHeader:   make(http.Header),
+		respHeader:  make(http.Header),
+		respTrailer: make(http.Header),
+	}
+}
+
+func (c *tripleServerTestConn) Spec() tri.Spec {
+	return tri.Spec{}
+}
+
+func (c *tripleServerTestConn) Peer() tri.Peer {
+	return tri.Peer{}
+}
+
+func (c *tripleServerTestConn) Receive(msg any) error {
+	if c.receiveFn != nil {
+		c.receiveFn(msg)
+	}
+	return nil
+}
+
+func (c *tripleServerTestConn) RequestHeader() http.Header {
+	return c.reqHeader
+}
+
+func (c *tripleServerTestConn) ExportableHeader() http.Header {
+	return c.reqHeader
+}
+
+func (c *tripleServerTestConn) Send(msg any) error {
+	c.sent = append(c.sent, msg)
+	return nil
+}
+
+func (c *tripleServerTestConn) ResponseHeader() http.Header {
+	return c.respHeader
+}
+
+func (c *tripleServerTestConn) ResponseTrailer() http.Header {
+	return c.respTrailer
+}
+
+func TestServerRegisterUnaryMethodHandler(t *testing.T) {
+	server := newServerForMethodHandlerTest()
+	invoker := &tripleServerTestInvoker{
+		invokeFn: func(ctx context.Context, invocation base.Invocation) result.Result {
+			assert.Equal(t, "UnaryMethod", invocation.MethodName())
+			assert.Equal(t, []any{"alice", 7}, invocation.Arguments())
+			assert.Equal(t, []string{"v1", "v2"}, invocation.Attachments()["x-test"])
+
+			ctxAttachments, ok := ctx.Value(constant.AttachmentKey).(map[string]any)
+			require.True(t, ok)
+			assert.Equal(t, []string{"v1", "v2"}, ctxAttachments["x-test"])
+
+			res := &result.RPCResult{}
+			res.SetResult("unary-ok")
+			res.SetAttachments(map[string]any{
+				"resp-one":   "val",
+				"resp-multi": []string{"a", "b"},
+				"resp-omit":  123,
+			})
+			return res
+		},
+	}
+	method := common.MethodInfo{
+		Name: "UnaryMethod",
+		Type: constant.CallUnary,
+		ReqInitFunc: func() any {
+			var name string
+			var age int
+			return []any{&name, &age}
+		},
+	}
+	procedure := "/svc/UnaryMethod"
+	server.registerMethodHandler(procedure, method, invoker)
+
+	conn := newTripleServerTestConn()
+	conn.reqHeader.Add("X-Test", "v1")
+	conn.reqHeader.Add("X-Test", "v2")
+	conn.receiveFn = func(msg any) {
+		args, ok := msg.([]any)
+		require.True(t, ok)
+		*(args[0].(*string)) = "alice"
+		*(args[1].(*int)) = 7
+	}
+	require.NoError(t, invokeRegisteredHandlerImplementation(server.triServer, procedure, conn))
+	require.Len(t, conn.sent, 1)
+	assert.Equal(t, []any{"unary-ok"}, conn.sent[0])
+	assert.Equal(t, []string{"val"}, conn.respTrailer.Values("resp-one"))
+	assert.Equal(t, []string{"a", "b"}, conn.respTrailer.Values("resp-multi"))
+	assert.Empty(t, conn.respTrailer.Values("resp-omit"))
+}
+
+func TestServerRegisterClientStreamMethodHandler(t *testing.T) {
+	server := newServerForMethodHandlerTest()
+	invoker := &tripleServerTestInvoker{
+		invokeFn: func(ctx context.Context, invocation base.Invocation) result.Result {
+			assert.Equal(t, "ClientStreamMethod", invocation.MethodName())
+			assert.Equal(t, []any{"client-token"}, invocation.Arguments())
+			assert.Equal(t, []string{"trace-123"}, invocation.Attachments()["trace-id"])
+			_, ok := ctx.Value(constant.AttachmentKey).(map[string]any)
+			assert.True(t, ok)
+
+			res := &result.RPCResult{}
+			res.SetResult("client-ok")
+			return res
+		},
+	}
+	method := common.MethodInfo{
+		Name: "ClientStreamMethod",
+		Type: constant.CallClientStream,
+		StreamInitFunc: func(baseStream any) any {
+			_, ok := baseStream.(*tri.ClientStream)
+			require.True(t, ok)
+			return "client-token"
+		},
+	}
+	procedure := "/svc/ClientStreamMethod"
+	server.registerMethodHandler(procedure, method, invoker)
+
+	conn := newTripleServerTestConn()
+	conn.reqHeader.Set("Trace-Id", "trace-123")
+	require.NoError(t, invokeRegisteredHandlerImplementation(server.triServer, procedure, conn))
+	require.Len(t, conn.sent, 1)
+	assert.Equal(t, []any{"client-ok"}, conn.sent[0])
+}
+
+func TestServerRegisterServerStreamMethodHandler(t *testing.T) {
+	type serverStreamReq struct {
+		Message string
+	}
+
+	server := newServerForMethodHandlerTest()
+	invoker := &tripleServerTestInvoker{
+		invokeFn: func(ctx context.Context, invocation base.Invocation) result.Result {
+			assert.Equal(t, "ServerStreamMethod", invocation.MethodName())
+			require.Len(t, invocation.Arguments(), 2)
+			req, ok := invocation.Arguments()[0].(*serverStreamReq)
+			require.True(t, ok)
+			assert.Equal(t, tripleServerTestFromClientMessage, req.Message)
+			assert.Equal(t, tripleServerTestServerToken, invocation.Arguments()[1])
+			assert.Equal(t, []string{"v"}, invocation.Attachments()["x-stream"])
+			_, ok = ctx.Value(constant.AttachmentKey).(map[string]any)
+			assert.True(t, ok)
+			return &result.RPCResult{}
+		},
+	}
+	method := common.MethodInfo{
+		Name: "ServerStreamMethod",
+		Type: constant.CallServerStream,
+		ReqInitFunc: func() any {
+			return &serverStreamReq{}
+		},
+		StreamInitFunc: func(baseStream any) any {
+			_, ok := baseStream.(*tri.ServerStream)
+			require.True(t, ok)
+			return tripleServerTestServerToken
+		},
+	}
+	procedure := "/svc/ServerStreamMethod"
+	server.registerMethodHandler(procedure, method, invoker)
+
+	conn := newTripleServerTestConn()
+	conn.reqHeader.Set("X-Stream", "v")
+	conn.receiveFn = func(msg any) {
+		req, ok := msg.(*serverStreamReq)
+		require.True(t, ok)
+		req.Message = tripleServerTestFromClientMessage
+	}
+	require.NoError(t, invokeRegisteredHandlerImplementation(server.triServer, procedure, conn))
+	assert.Empty(t, conn.sent)
+}
+
+func TestServerRegisterBidiStreamMethodHandler(t *testing.T) {
+	server := newServerForMethodHandlerTest()
+	invoker := &tripleServerTestInvoker{
+		invokeFn: func(ctx context.Context, invocation base.Invocation) result.Result {
+			assert.Equal(t, "BidiStreamMethod", invocation.MethodName())
+			assert.Equal(t, []any{tripleServerTestBidiToken}, invocation.Arguments())
+			assert.Equal(t, []string{tripleServerTestBidiValue}, invocation.Attachments()["x-bidi"])
+			_, ok := ctx.Value(constant.AttachmentKey).(map[string]any)
+			assert.True(t, ok)
+			return &result.RPCResult{}
+		},
+	}
+	method := common.MethodInfo{
+		Name: "BidiStreamMethod",
+		Type: constant.CallBidiStream,
+		StreamInitFunc: func(baseStream any) any {
+			_, ok := baseStream.(*tri.BidiStream)
+			require.True(t, ok)
+			return tripleServerTestBidiToken
+		},
+	}
+	procedure := "/svc/BidiStreamMethod"
+	server.registerMethodHandler(procedure, method, invoker)
+
+	conn := newTripleServerTestConn()
+	conn.reqHeader.Set("X-Bidi", tripleServerTestBidiValue)
+	require.NoError(t, invokeRegisteredHandlerImplementation(server.triServer, procedure, conn))
+	assert.Empty(t, conn.sent)
+}
+
+func TestServerRegisterMethodHandlerUnknownType(t *testing.T) {
+	server := newServerForMethodHandlerTest()
+	procedure := "/svc/Unknown"
+	server.registerMethodHandler(procedure, common.MethodInfo{
+		Name: "UnknownMethod",
+		Type: "unknown",
+	}, &tripleServerTestInvoker{})
+
+	_, ok := getServerHandler(server.triServer, procedure)
+	assert.False(t, ok)
+}
+
+func TestServerHandleServiceWithInfoFallbackHitsStreamingHandlers(t *testing.T) {
+	type streamReq struct {
+		Message string
+	}
+
+	server := newServerForMethodHandlerTest()
+	calledMethods := make([]string, 0, 2)
+	invoker := &tripleServerTestInvoker{
+		invokeFn: func(ctx context.Context, invocation base.Invocation) result.Result {
+			calledMethods = append(calledMethods, invocation.MethodName())
+			switch invocation.MethodName() {
+			case "CountUp":
+				require.Len(t, invocation.Arguments(), 2)
+				req, ok := invocation.Arguments()[0].(*streamReq)
+				require.True(t, ok)
+				assert.Equal(t, tripleServerTestFromClientMessage, req.Message)
+				assert.Equal(t, tripleServerTestServerToken, invocation.Arguments()[1])
+				assert.Equal(t, []string{"stream-v"}, invocation.Attachments()["x-stream"])
+			case "CumSum":
+				require.Len(t, invocation.Arguments(), 1)
+				assert.Equal(t, tripleServerTestBidiToken, invocation.Arguments()[0])
+				assert.Equal(t, []string{tripleServerTestBidiValue}, invocation.Attachments()["x-bidi"])
+			default:
+				t.Fatalf("unexpected method: %s", invocation.MethodName())
+			}
+
+			ctxAttachments, ok := ctx.Value(constant.AttachmentKey).(map[string]any)
+			require.True(t, ok)
+			assert.NotEmpty(t, ctxAttachments)
+			return &result.RPCResult{}
+		},
+	}
+	info := &common.ServiceInfo{
+		Methods: []common.MethodInfo{
+			{
+				Name: "CountUp",
+				Type: constant.CallServerStream,
+				ReqInitFunc: func() any {
+					return &streamReq{}
+				},
+				StreamInitFunc: func(baseStream any) any {
+					_, ok := baseStream.(*tri.ServerStream)
+					require.True(t, ok)
+					return tripleServerTestServerToken
+				},
+			},
+			{
+				Name: "CumSum",
+				Type: constant.CallBidiStream,
+				StreamInitFunc: func(baseStream any) any {
+					_, ok := baseStream.(*tri.BidiStream)
+					require.True(t, ok)
+					return tripleServerTestBidiToken
+				},
+			},
+		},
+	}
+	server.handleServiceWithInfo("svc.Fallback", invoker, info)
+
+	serverStreamConn := newTripleServerTestConn()
+	serverStreamConn.reqHeader.Set("X-Stream", "stream-v")
+	serverStreamConn.receiveFn = func(msg any) {
+		req, ok := msg.(*streamReq)
+		require.True(t, ok)
+		req.Message = tripleServerTestFromClientMessage
+	}
+	pattern, err := invokeRegisteredHandlerImplementationByRequestPath(
+		server.triServer,
+		"/svc.Fallback/countUp",
+		serverStreamConn,
+	)
+	require.NoError(t, err)
+	assert.Equal(t, "/svc.Fallback/CountUp", pattern)
+
+	bidiConn := newTripleServerTestConn()
+	bidiConn.reqHeader.Set("X-Bidi", tripleServerTestBidiValue)
+	pattern, err = invokeRegisteredHandlerImplementationByRequestPath(
+		server.triServer,
+		"/svc.Fallback/cumSum",
+		bidiConn,
+	)
+	require.NoError(t, err)
+	assert.Equal(t, "/svc.Fallback/CumSum", pattern)
+
+	assert.Equal(t, []string{"CountUp", "CumSum"}, calledMethods)
+}
+
+func newServerForMethodHandlerTest() *Server {
+	return &Server{triServer: tri.NewServer("127.0.0.1:0", nil)}
+}
+
+func TestExtractUnaryInvocationArgs(t *testing.T) {
+	t.Run("from non-idl argument slice", func(t *testing.T) {
+		name := "alice"
+		age := 18
+		args := extractUnaryInvocationArgs([]any{&name, &age})
+		assert.Equal(t, []any{"alice", 18}, args)
+	})
+
+	t.Run("from single message in idl mode", func(t *testing.T) {
+		msg := struct{ Name string }{Name: "idl"}
+		args := extractUnaryInvocationArgs(msg)
+		assert.Equal(t, []any{msg}, args)
+	})
+}
+
+func TestWrapTripleResponse(t *testing.T) {
+	resp := tri.NewResponse("already-wrapped")
+	assert.Same(t, resp, wrapTripleResponse(resp))
+
+	wrapped := wrapTripleResponse("plain-result")
+	assert.Equal(t, []any{"plain-result"}, wrapped.Msg)
+}
+
+func TestAppendTripleOutgoingAttachments(t *testing.T) {
+	ctx := tri.NewOutgoingContext(context.Background(), make(http.Header))
+	appendTripleOutgoingAttachments(ctx, map[string]any{
+		"one":   "1",
+		"multi": []string{"a", "b"},
+		"omit":  100,
+	})
+
+	outgoing := tri.ExtractFromOutgoingContext(ctx)
+	require.NotNil(t, outgoing)
+	assert.Equal(t, []string{"1"}, outgoing.Values("one"))
+	assert.Equal(t, []string{"a", "b"}, outgoing.Values("multi"))
+	assert.Empty(t, outgoing.Values("omit"))
+}
+
+const tripleServerDefaultImplementationKey = "/"
+
+// These helpers execute the default registered implementation directly so the
+// tests can verify registerMethodHandler's invocation wiring without depending
+// on protocol-specific HTTP framing details.
+func invokeRegisteredHandlerImplementation(triServer *tri.Server, procedure string, conn tri.StreamingHandlerConn) error {
+	handler, ok := getServerHandler(triServer, procedure)
+	if !ok {
+		return fmt.Errorf("handler for procedure %s not found", procedure)
+	}
+	implementation, ok := getDefaultHandlerImplementation(handler)
+	if !ok {
+		return fmt.Errorf("default implementation for procedure %s not found", procedure)
+	}
+	return implementation(context.Background(), conn)
+}
+
+func invokeRegisteredHandlerImplementationByRequestPath(
+	triServer *tri.Server,
+	requestPath string,
+	conn tri.StreamingHandlerConn,
+) (string, error) {
+	handler, pattern, ok := getServerHandlerByRequestPath(triServer, requestPath)
+	if !ok {
+		return "", fmt.Errorf("handler for request path %s not found", requestPath)
+	}
+	implementation, ok := getDefaultHandlerImplementation(handler)
+	if !ok {
+		return "", fmt.Errorf("default implementation for request path %s not found", requestPath)
+	}
+	return pattern, implementation(context.Background(), conn)
+}
+
+func getServerHandler(triServer *tri.Server, procedure string) (*tri.Handler, bool) {
+	if triServer == nil {
+		return nil, false
+	}
+	handlersField := reflect.ValueOf(triServer).Elem().FieldByName("handlers")
+	handlersValue, ok := extractUnexportedValue(handlersField)
+	if !ok {
+		return nil, false
+	}
+	handlers, ok := handlersValue.Interface().(map[string]*tri.Handler)
+	if !ok {
+		return nil, false
+	}
+	handler, ok := handlers[procedure]
+	return handler, ok
+}
+
+func getServerHandlerByRequestPath(triServer *tri.Server, requestPath string) (*tri.Handler, string, bool) {
+	if triServer == nil {
+		return nil, "", false
+	}
+	muxField := reflect.ValueOf(triServer).Elem().FieldByName("mux")
+	muxValue, ok := extractUnexportedValue(muxField)
+	if !ok || !muxValue.IsValid() || muxValue.IsNil() {
+		return nil, "", false
+	}
+
+	handlerMethod := muxValue.MethodByName("Handler")
+	if !handlerMethod.IsValid() {
+		return nil, "", false
+	}
+	req := httptest.NewRequest(http.MethodPost, requestPath, nil)
+	results := handlerMethod.Call([]reflect.Value{reflect.ValueOf(req)})
+	if len(results) != 2 {
+		return nil, "", false
+	}
+
+	handler, ok := results[0].Interface().(http.Handler)
+	if !ok || handler == nil {
+		return nil, "", false
+	}
+	pattern, ok := results[1].Interface().(string)
+	if !ok || pattern == "" {
+		return nil, "", false
+	}
+	triHandler, ok := handler.(*tri.Handler)
+	if !ok {
+		return nil, "", false
+	}
+
+	return triHandler, pattern, true
+}
+
+func getDefaultHandlerImplementation(handler *tri.Handler) (tri.StreamingHandlerFunc, bool) {
+	if handler == nil {
+		return nil, false
+	}
+	implField := reflect.ValueOf(handler).Elem().FieldByName("implementations")
+	implValue, ok := extractUnexportedValue(implField)
+	if !ok {
+		return nil, false
+	}
+	implementations, ok := implValue.Interface().(map[string]tri.StreamingHandlerFunc)
+	if !ok {
+		return nil, false
+	}
+	implementation, ok := implementations[tripleServerDefaultImplementationKey]
+	return implementation, ok
+}
+
+func extractUnexportedValue(field reflect.Value) (reflect.Value, bool) {
+	if !field.IsValid() || !field.CanAddr() {
+		return reflect.Value{}, false
+	}
+	return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem(), true
+}
diff --git a/protocol/triple/triple_protocol/method_route_mux.go b/protocol/triple/triple_protocol/method_route_mux.go
new file mode 100644
index 000000000..b4be12669
--- /dev/null
+++ b/protocol/triple/triple_protocol/method_route_mux.go
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package triple_protocol
+
+import (
+	"net/http"
+	"sync"
+	"unicode"
+	"unicode/utf8"
+)
+
+// methodRouteMux wraps http.ServeMux to provide case-insensitive procedure
+// routing without duplicating method metadata in higher layers.
+//
+// Lookup order:
+//  1. exact match via http.ServeMux
+//  2. lowercase-first-method fallback via an internal index
+type methodRouteMux struct {
+	exact *http.ServeMux
+
+	mu    sync.RWMutex
+	lower map[string]methodRouteEntry
+}
+
+type methodRouteEntry struct {
+	pattern string
+	handler http.Handler
+}
+
+func newMethodRouteMux() *methodRouteMux {
+	return &methodRouteMux{
+		exact: http.NewServeMux(),
+		lower: make(map[string]methodRouteEntry),
+	}
+}
+
+// Handle registers the handler in the exact mux and the lowercase-first-method
+// fallback index. If two patterns collide after fallback normalization, the
+// first registration wins while exact matching keeps the original behavior.
+func (m *methodRouteMux) Handle(pattern string, handler http.Handler) {
+	m.exact.Handle(pattern, handler)
+
+	lowerKey := normalizeMethodRouteKey(pattern)
+	m.mu.Lock()
+	defer m.mu.Unlock()
+	// Keep the first registration for a collided fallback key so mixed-case
+	// registrations from different generators (for example Go/Java stubs) keep
+	// deterministic behavior.
+	if _, exists := m.lower[lowerKey]; !exists {
+		m.lower[lowerKey] = methodRouteEntry{
+			pattern: pattern,
+			handler: handler,
+		}
+	}
+}
+
+func (m *methodRouteMux) Handler(r *http.Request) (http.Handler, string) {
+	if handler, pattern := m.exact.Handler(r); pattern != "" {
+		return handler, pattern
+	}
+
+	lowerKey := normalizeMethodRouteKey(r.URL.Path)
+	m.mu.RLock()
+	entry, ok := m.lower[lowerKey]
+	m.mu.RUnlock()
+	if ok {
+		return entry.handler, entry.pattern
+	}
+
+	return http.NotFoundHandler(), ""
+}
+
+func (m *methodRouteMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	handler, pattern := m.Handler(r)
+	if pattern != "" {
+		handler.ServeHTTP(w, r)
+		return
+	}
+
+	http.NotFound(w, r)
+}
+
+func normalizeMethodRouteKey(path string) string {
+	lastSlash := -1
+	for i := len(path) - 1; i >= 0; i-- {
+		if path[i] == '/' {
+			lastSlash = i
+			break
+		}
+	}
+	if lastSlash < 0 || lastSlash == len(path)-1 {
+		return path
+	}
+	return path[:lastSlash+1] + lowerFirstRune(path[lastSlash+1:])
+}
+
+func lowerFirstRune(s string) string {
+	r, size := utf8.DecodeRuneInString(s)
+	if r == utf8.RuneError && size == 0 {
+		return s
+	}
+	return string(unicode.ToLower(r)) + s[size:]
+}
diff --git a/protocol/triple/triple_protocol/method_route_mux_test.go b/protocol/triple/triple_protocol/method_route_mux_test.go
new file mode 100644
index 000000000..331d7a8c9
--- /dev/null
+++ b/protocol/triple/triple_protocol/method_route_mux_test.go
@@ -0,0 +1,208 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package triple_protocol
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"testing"
+)
+
+import (
+	"github.com/stretchr/testify/assert"
+)
+
+const (
+	methodRouteMuxGetUserPath      = "/Service/GetUser"
+	methodRouteMuxGetUserLowerPath = "/Service/getUser"
+	methodRouteMuxNotFoundBody     = "404 page not found\n"
+)
+
+func TestMethodRouteMux(t *testing.T) {
+	tests := []struct {
+		name           string
+		register       func(*methodRouteMux)
+		requestPath    string
+		wantStatusCode int
+		wantBody       string
+	}{
+		{
+			name: "exact match",
+			register: func(m *methodRouteMux) {
+				m.Handle(methodRouteMuxGetUserPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+					_, _ = w.Write([]byte("exact"))
+				}))
+			},
+			requestPath:    methodRouteMuxGetUserPath,
+			wantStatusCode: http.StatusOK,
+			wantBody:       "exact",
+		},
+		{
+			name: "lowercase first rune fallback",
+			register: func(m *methodRouteMux) {
+				m.Handle(methodRouteMuxGetUserPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+					_, _ = w.Write([]byte("fallback"))
+				}))
+			},
+			requestPath:    methodRouteMuxGetUserLowerPath,
+			wantStatusCode: http.StatusOK,
+			wantBody:       "fallback",
+		},
+		{
+			name: "uppercase first rune fallback",
+			register: func(m *methodRouteMux) {
+				m.Handle(methodRouteMuxGetUserLowerPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+					_, _ = w.Write([]byte("fallback"))
+				}))
+			},
+			requestPath:    methodRouteMuxGetUserPath,
+			wantStatusCode: http.StatusOK,
+			wantBody:       "fallback",
+		},
+		{
+			name: "full lowercase fallback",
+			register: func(m *methodRouteMux) {
+				m.Handle(methodRouteMuxGetUserPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+					_, _ = w.Write([]byte("fallback"))
+				}))
+			},
+			requestPath:    "/service/getuser",
+			wantStatusCode: http.StatusNotFound,
+			wantBody:       methodRouteMuxNotFoundBody,
+		},
+		{
+			name: "not found",
+			register: func(m *methodRouteMux) {
+				m.Handle(methodRouteMuxGetUserPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+					_, _ = w.Write([]byte("unexpected"))
+				}))
+			},
+			requestPath:    "/Service/Delete",
+			wantStatusCode: http.StatusNotFound,
+			wantBody:       methodRouteMuxNotFoundBody,
+		},
+		{
+			name: "methods differing beyond first rune remain distinct",
+			register: func(m *methodRouteMux) {
+				m.Handle("/S/GetUser", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+					_, _ = w.Write([]byte("first"))
+				}))
+				m.Handle("/S/Getuser", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+					_, _ = w.Write([]byte("second"))
+				}))
+			},
+			requestPath:    "/S/getuser",
+			wantStatusCode: http.StatusOK,
+			wantBody:       "second",
+		},
+		{
+			name: "service path remains case sensitive",
+			register: func(m *methodRouteMux) {
+				m.Handle(methodRouteMuxGetUserPath, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+					_, _ = w.Write([]byte("unexpected"))
+				}))
+			},
+			requestPath:    "/service/getUser",
+			wantStatusCode: http.StatusNotFound,
+			wantBody:       methodRouteMuxNotFoundBody,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			mux := newMethodRouteMux()
+			tt.register(mux)
+
+			req := httptest.NewRequest(http.MethodPost, tt.requestPath, nil)
+			resp := httptest.NewRecorder()
+			mux.ServeHTTP(resp, req)
+
+			assert.Equal(t, tt.wantStatusCode, resp.Code)
+			assert.Equal(t, tt.wantBody, resp.Body.String())
+		})
+	}
+}
+
+func TestNormalizeMethodRouteKeyEdgeCases(t *testing.T) {
+	tests := []struct {
+		name string
+		path string
+		want string
+	}{
+		{
+			name: "path without slash remains unchanged",
+			path: "GetUser",
+			want: "GetUser",
+		},
+		{
+			name: "path ending with slash remains unchanged",
+			path: "/Service/",
+			want: "/Service/",
+		},
+		{
+			name: "root path remains unchanged",
+			path: "/",
+			want: "/",
+		},
+		{
+			name: "normal path lowercases first rune of method only",
+			path: methodRouteMuxGetUserPath,
+			want: methodRouteMuxGetUserLowerPath,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.want, normalizeMethodRouteKey(tt.path))
+		})
+	}
+}
+
+func TestLowerFirstRuneEdgeCases(t *testing.T) {
+	assert.Empty(t, lowerFirstRune(""))
+	assert.Equal(t, "abc", lowerFirstRune("Abc"))
+	assert.Equal(t, "äbc", lowerFirstRune("Äbc"))
+}
+
+func TestMethodRouteMuxFallbackCollisionFirstRegistrationWins(t *testing.T) {
+	mux := newMethodRouteMux()
+	firstPattern := methodRouteMuxGetUserPath
+	secondPattern := methodRouteMuxGetUserLowerPath
+
+	mux.Handle(firstPattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte("first"))
+	}))
+	mux.Handle(secondPattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		_, _ = w.Write([]byte("second"))
+	}))
+
+	lowerKey := normalizeMethodRouteKey(firstPattern)
+	mux.mu.RLock()
+	entry, ok := mux.lower[lowerKey]
+	mux.mu.RUnlock()
+
+	if !assert.True(t, ok) {
+		return
+	}
+	assert.Equal(t, firstPattern, entry.pattern)
+
+	req := httptest.NewRequest(http.MethodPost, firstPattern, nil)
+	resp := httptest.NewRecorder()
+	entry.handler.ServeHTTP(resp, req)
+	assert.Equal(t, "first", resp.Body.String())
+}
diff --git a/protocol/triple/triple_protocol/server.go b/protocol/triple/triple_protocol/server.go
index 1346f9b0c..ef162ff8c 100644
--- a/protocol/triple/triple_protocol/server.go
+++ b/protocol/triple/triple_protocol/server.go
@@ -45,7 +45,7 @@ import (
 
 type Server struct {
 	addr         string
-	mux          *http.ServeMux
+	mux          *methodRouteMux
 	handlers     map[string]*Handler
 	httpSrv      *http.Server
 	http3Srv     *http3.Server
@@ -338,7 +338,7 @@ func (s *Server) GracefulStop(ctx context.Context) error {
 
 func NewServer(addr string, tripleConf *global.TripleConfig) *Server {
 	return &Server{
-		mux:          http.NewServeMux(),
+		mux:          newMethodRouteMux(),
 		addr:         addr,
 		handlers:     make(map[string]*Handler),
 		tripleConfig: tripleConf,
diff --git a/protocol/triple/triple_protocol/triple_ext_test.go b/protocol/triple/triple_protocol/triple_ext_test.go
index bcc506342..e26a17de9 100644
--- a/protocol/triple/triple_protocol/triple_ext_test.go
+++ b/protocol/triple/triple_protocol/triple_ext_test.go
@@ -23,6 +23,7 @@ import (
 	"fmt"
 	"io"
 	"math/rand"
+	"net"
 	"net/http"
 	"net/http/httptest"
 	"strings"
@@ -790,8 +791,17 @@ func TestGRPCMissingTrailersError(t *testing.T) {
 
 func TestUnavailableIfHostInvalid(t *testing.T) {
 	t.Parallel()
+	transport := http.DefaultTransport.(*http.Transport).Clone()
+	transport.Proxy = nil
+	transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) {
+		return nil, &net.DNSError{
+			Err:        "no such host",
+			Name:       "api.invalid",
+			IsNotFound: true,
+		}
+	}
 	client := pingv1connect.NewPingServiceClient(
-		http.DefaultClient,
+		&http.Client{Transport: transport},
 		"https://api.invalid/",
 	)
 	err := client.Ping(
diff --git a/server/server.go b/server/server.go
index c4bba3950..eacb0c391 100644
--- a/server/server.go
+++ b/server/server.go
@@ -23,7 +23,6 @@ import (
 	"reflect"
 	"sort"
 	"strconv"
-	"strings"
 	"sync"
 )
 
@@ -237,32 +236,33 @@ func createReflectionMethodFunc(method reflect.Method) func(ctx context.Context,
 	}
 }
 
-// Add a method with a name of a different first-letter case
-// to achieve interoperability with java
-// TODO: The method name case sensitivity in Dubbo-java should be addressed.
-// We ought to make changes to handle this issue.
+// enhanceServiceInfo fills in missing MethodFunc entries via reflection.
+// Case-insensitive Triple routing is handled in the transport-layer route mux,
+// but lowercase-first ServiceInfo method names still need MethodFunc backfill so
+// reflection-based invocation can reach the exported Go method.
 func enhanceServiceInfo(info *common.ServiceInfo) *common.ServiceInfo {
 	if info == nil {
 		return info
 	}
 
-	// Get service type for reflection-based method calls
 	var svcType reflect.Type
 	if info.ServiceType != nil {
 		svcType = reflect.TypeOf(info.ServiceType)
 	}
 
-	// Build method map for reflection lookup
+	// Build method map for reflection lookup.
+	// Keep the first-rune-swapped alias for lowercase-first ServiceInfo names
+	// (for example "sayHello" -> "SayHello") without duplicating metadata.
 	methodMap := make(map[string]reflect.Method)
 	if svcType != nil {
 		for i := 0; i < svcType.NumMethod(); i++ {
 			m := svcType.Method(i)
 			methodMap[m.Name] = m
-			methodMap[strings.ToLower(m.Name)] = m
+			methodMap[dubboutil.SwapCaseFirstRune(m.Name)] = m
 		}
 	}
 
-	// Add MethodFunc to methods that don't have it
+	// Fill in MethodFunc for methods that don't already have one.
 	for i := range info.Methods {
 		if info.Methods[i].MethodFunc == nil && svcType != nil {
 			if reflectMethod, ok := methodMap[info.Methods[i].Name]; ok {
@@ -271,22 +271,6 @@ func enhanceServiceInfo(info *common.ServiceInfo) *common.ServiceInfo {
 		}
 	}
 
-	// Create additional methods with swapped-case names for Java interoperability
-	var additionalMethods []common.MethodInfo
-	for _, method := range info.Methods {
-		newMethod := method
-		newMethod.Name = dubboutil.SwapCaseFirstRune(method.Name)
-		if method.MethodFunc != nil {
-			newMethod.MethodFunc = method.MethodFunc
-		} else if svcType != nil {
-			if reflectMethod, ok := methodMap[dubboutil.SwapCaseFirstRune(method.Name)]; ok {
-				newMethod.MethodFunc = createReflectionMethodFunc(reflectMethod)
-			}
-		}
-		additionalMethods = append(additionalMethods, newMethod)
-	}
-	info.Methods = append(info.Methods, additionalMethods...)
-
 	return info
 }
 
diff --git a/server/server_test.go b/server/server_test.go
index a12daa36c..edced826a 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -18,6 +18,7 @@
 package server
 
 import (
+	"context"
 	"reflect"
 	"strconv"
 	"sync"
@@ -299,11 +300,57 @@ func TestEnhanceServiceInfo(t *testing.T) {
 
 	result := enhanceServiceInfo(info)
 	assert.NotNil(t, result)
-	// Should have doubled methods (original + case-swapped)
-	assert.Len(t, result.Methods, 2)
+	// ServiceInfo.Methods must only contain original method names.
+	// Swapped-case aliases are registered at the transport layer, not here.
+	assert.Len(t, result.Methods, 1)
 	assert.Equal(t, "sayHello", result.Methods[0].Name)
-	// The swapped version should have capitalized first letter
-	assert.Equal(t, "SayHello", result.Methods[1].Name)
+}
+
+// greetServiceForTest is a minimal service used to verify MethodFunc backfill.
+type greetServiceForTest struct{}
+
+func (g *greetServiceForTest) Greet(ctx context.Context, req string) (string, error) {
+	return req, nil
+}
+
+func (g *greetServiceForTest) Reference() string { return "greetServiceForTest" }
+
+// TestEnhanceServiceInfoMethodFuncBackfillExactName verifies that
+// enhanceServiceInfo fills in MethodFunc when the ServiceInfo method name
+// matches the Go exported method name exactly (PascalCase).
+func TestEnhanceServiceInfoMethodFuncBackfillExactName(t *testing.T) {
+	svc := &greetServiceForTest{}
+	info := &common.ServiceInfo{
+		ServiceType: svc,
+		Methods: []common.MethodInfo{
+			{Name: "Greet"}, // exact Go name — must be found
+		},
+	}
+
+	result := enhanceServiceInfo(info)
+	assert.NotNil(t, result)
+	assert.Len(t, result.Methods, 1)
+	assert.NotNil(t, result.Methods[0].MethodFunc,
+		"MethodFunc must be filled in for exact-name match to avoid nil-func panic")
+}
+
+// TestEnhanceServiceInfoMethodFuncBackfillJavaStyleName verifies that
+// enhanceServiceInfo still fills in MethodFunc for lowercase-first method names
+// so reflection-based invocation can reach the exported Go method.
+func TestEnhanceServiceInfoMethodFuncBackfillJavaStyleName(t *testing.T) {
+	svc := &greetServiceForTest{}
+	info := &common.ServiceInfo{
+		ServiceType: svc,
+		Methods: []common.MethodInfo{
+			{Name: "greet"}, // Java/Dubbo-style lowercase-first name
+		},
+	}
+
+	result := enhanceServiceInfo(info)
+	assert.NotNil(t, result)
+	assert.Len(t, result.Methods, 1)
+	assert.NotNil(t, result.Methods[0].MethodFunc,
+		"MethodFunc must be found via swapped-case lookup to avoid nil-func panic on lowercase-first method names")
 }
 
 // Test getMetadataPort with default protocol
diff --git a/server/triple_case_route_integration_test.go b/server/triple_case_route_integration_test.go
new file mode 100644
index 000000000..db8604ca4
--- /dev/null
+++ b/server/triple_case_route_integration_test.go
@@ -0,0 +1,255 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package server
+
+import (
+	"context"
+	"io"
+	"net"
+	"net/http"
+	"strconv"
+	"strings"
+	"testing"
+	"time"
+)
+
+import (
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"google.golang.org/protobuf/types/known/emptypb"
+	"google.golang.org/protobuf/types/known/wrapperspb"
+)
+
+import (
+	"dubbo.apache.org/dubbo-go/v3/common"
+	"dubbo.apache.org/dubbo-go/v3/common/constant"
+	"dubbo.apache.org/dubbo-go/v3/common/extension"
+	_ "dubbo.apache.org/dubbo-go/v3/filter/accesslog"         // Register default provider filters for exportServices in this integration test.
+	_ "dubbo.apache.org/dubbo-go/v3/filter/echo"              // Register default provider filters for exportServices in this integration test.
+	_ "dubbo.apache.org/dubbo-go/v3/filter/exec_limit"        // Register default provider filters for exportServices in this integration test.
+	_ "dubbo.apache.org/dubbo-go/v3/filter/generic"           // Register default provider filters for exportServices in this integration test.
+	_ "dubbo.apache.org/dubbo-go/v3/filter/graceful_shutdown" // Register default provider filters for exportServices in this integration test.
+	_ "dubbo.apache.org/dubbo-go/v3/filter/token"             // Register default provider filters for exportServices in this integration test.
+	_ "dubbo.apache.org/dubbo-go/v3/filter/tps"               // Register default provider filters for exportServices in this integration test.
+	"dubbo.apache.org/dubbo-go/v3/protocol"
+	_ "dubbo.apache.org/dubbo-go/v3/protocol/protocolwrapper" // Register protocol wrappers used by exportServices in this integration test.
+	_ "dubbo.apache.org/dubbo-go/v3/protocol/triple"          // Register the Triple protocol used by exportServices in this integration test.
+	tri "dubbo.apache.org/dubbo-go/v3/protocol/triple/triple_protocol"
+	_ "dubbo.apache.org/dubbo-go/v3/proxy/proxy_factory" // Register the default proxy factory used during export in this integration test.
+)
+
+const (
+	tripleCaseRouteHelloBody    = "\"Hello test\""
+	tripleCaseRouteNotFoundBody = "404 page not found\n"
+)
+
+type TripleCaseRouteService struct{}
+
+func (s *TripleCaseRouteService) Reference() string {
+	return "com.example.GreetService"
+}
+
+func (s *TripleCaseRouteService) SayHello(context.Context, *emptypb.Empty) (*wrapperspb.StringValue, error) {
+	return wrapperspb.String("Hello test"), nil
+}
+
+func TestTripleCaseInsensitiveRoute(t *testing.T) {
+	t.Run("pascal case registration accepts lowercase fallback", func(t *testing.T) {
+		runTripleCaseRouteIntegration(
+			t,
+			"com.example.GreetService",
+			"SayHello",
+			"/com.example.GreetService/SayHello",
+			[]tripleRouteExpectation{
+				{
+					name:       "canonical method name",
+					path:       "/com.example.GreetService/SayHello",
+					wantStatus: http.StatusOK,
+					wantBody:   tripleCaseRouteHelloBody,
+				},
+				{
+					name:       "lowercase method name fallback",
+					path:       "/com.example.GreetService/sayHello",
+					wantStatus: http.StatusOK,
+					wantBody:   tripleCaseRouteHelloBody,
+				},
+				{
+					name:       "unknown method remains not found",
+					path:       "/com.example.GreetService/DeleteUser",
+					wantStatus: http.StatusNotFound,
+					wantBody:   tripleCaseRouteNotFoundBody,
+				},
+			},
+		)
+	})
+
+	t.Run("camel case registration accepts uppercase fallback", func(t *testing.T) {
+		runTripleCaseRouteIntegration(
+			t,
+			"com.example.JavaStyleGreetService",
+			"sayHello",
+			"/com.example.JavaStyleGreetService/sayHello",
+			[]tripleRouteExpectation{
+				{
+					name:       "registered camel case method name",
+					path:       "/com.example.JavaStyleGreetService/sayHello",
+					wantStatus: http.StatusOK,
+					wantBody:   tripleCaseRouteHelloBody,
+				},
+				{
+					name:       "uppercase method name fallback",
+					path:       "/com.example.JavaStyleGreetService/SayHello",
+					wantStatus: http.StatusOK,
+					wantBody:   tripleCaseRouteHelloBody,
+				},
+				{
+					name:       "unknown method remains not found",
+					path:       "/com.example.JavaStyleGreetService/DeleteUser",
+					wantStatus: http.StatusNotFound,
+					wantBody:   tripleCaseRouteNotFoundBody,
+				},
+			},
+		)
+	})
+}
+
+type tripleRouteExpectation struct {
+	name       string
+	path       string
+	wantStatus int
+	wantBody   string
+}
+
+func runTripleCaseRouteIntegration(
+	t *testing.T,
+	interfaceName string,
+	methodName string,
+	readyPath string,
+	expectations []tripleRouteExpectation,
+) {
+	t.Helper()
+
+	port := testFreePort(t)
+	srv, err := NewServer(
+		WithServerProtocol(
+			protocol.WithTriple(),
+			protocol.WithIp("127.0.0.1"),
+			protocol.WithPort(port),
+		),
+	)
+	require.NoError(t, err)
+
+	service := &TripleCaseRouteService{}
+	info := &common.ServiceInfo{
+		InterfaceName: interfaceName,
+		ServiceType:   service,
+		Methods: []common.MethodInfo{
+			{
+				Name: methodName,
+				Type: constant.CallUnary,
+				ReqInitFunc: func() any {
+					return &emptypb.Empty{}
+				},
+				MethodFunc: func(ctx context.Context, args []any, handler any) (any, error) {
+					req := args[0].(*emptypb.Empty)
+					res, callErr := handler.(*TripleCaseRouteService).SayHello(ctx, req)
+					if callErr != nil {
+						return nil, callErr
+					}
+					return tri.NewResponse(res), nil
+				},
+			},
+		},
+	}
+
+	err = srv.Register(service, info, WithInterface(info.InterfaceName), WithNotRegister())
+	require.NoError(t, err)
+	err = srv.exportServices()
+	require.NoError(t, err)
+
+	t.Cleanup(func() {
+		extension.GetProtocol(constant.TriProtocol).Destroy()
+	})
+
+	client := &http.Client{Timeout: 2 * time.Second}
+	baseURL := "http://127.0.0.1:" + strconv.Itoa(port)
+	waitTripleRouteReady(t, client, baseURL+readyPath)
+
+	for _, tt := range expectations {
+		t.Run(tt.name, func(t *testing.T) {
+			status, body, reqErr := tripleRouteRequest(client, baseURL+tt.path)
+			require.NoError(t, reqErr)
+			assert.Equal(t, tt.wantStatus, status)
+			assert.Equal(t, tt.wantBody, body)
+		})
+	}
+}
+
+func testFreePort(t *testing.T) int {
+	t.Helper()
+
+	listener, err := net.Listen("tcp", "127.0.0.1:0")
+	require.NoError(t, err)
+	defer func() {
+		_ = listener.Close()
+	}()
+
+	return listener.Addr().(*net.TCPAddr).Port
+}
+
+func tripleRouteRequest(client *http.Client, url string) (int, string, error) {
+	req, err := http.NewRequest(http.MethodPost, url, strings.NewReader("{}"))
+	if err != nil {
+		return 0, "", err
+	}
+	req.Header.Set("Content-Type", "application/json")
+
+	resp, err := client.Do(req)
+	if err != nil {
+		return 0, "", err
+	}
+	defer func() {
+		_ = resp.Body.Close()
+	}()
+
+	body, err := io.ReadAll(resp.Body)
+	if err != nil {
+		return 0, "", err
+	}
+	return resp.StatusCode, string(body), nil
+}
+
+func waitTripleRouteReady(t *testing.T, client *http.Client, url string) {
+	t.Helper()
+
+	deadline := time.Now().Add(5 * time.Second)
+	var lastStatus int
+	var lastBody string
+	var lastErr error
+
+	for time.Now().Before(deadline) {
+		lastStatus, lastBody, lastErr = tripleRouteRequest(client, url)
+		if lastErr == nil && lastStatus == http.StatusOK && lastBody == tripleCaseRouteHelloBody {
+			return
+		}
+		time.Sleep(50 * time.Millisecond)
+	}
+
+	t.Fatalf("triple route not ready: status=%d body=%q err=%v", lastStatus, lastBody, lastErr)
+}