Skip to content

Commit 03916a9

Browse files
committed
fix(triple): move SwapCase alias registration to transport layer (apache#3162)
Problem: Triple protocol routes requests by URL path (/{interface}/{method}). Go providers export PascalCase names (GetUser) while Java clients send camelCase paths (getUser), causing 404 for alias-path calls. The previous workaround (PR apache#3154) appended swapped-case MethodInfo entries to ServiceInfo.Methods, polluting registry metadata and gRPC reflection with duplicate method names (GetUser + getUser). Solution: - enhanceServiceInfo: remove SwapCase alias appending; only fill in missing MethodFunc via reflection. ServiceInfo.Methods stays clean. - handleServiceWithInfo: extract registerMethodHandler; register each method under both the canonical path and the first-rune-swapped path. The alias path reuses the original MethodInfo unchanged so that: 1. infoProxyInvoker can look up the method by its canonical name (proxy/proxy_factory/default.go uses ServiceInfo.Methods for the lookup table). 2. Method-scoped provider config (methods.<Name>.*) continues to apply; config is keyed by the canonical Go/proto name in server/action.go, and filters read via invocation.MethodName(). - Restore swapped-case index in enhanceServiceInfo methodMap so that ServiceInfo entries using Java-style lowercase names (e.g. "ping") can still find the exported Go method ("Ping") for MethodFunc backfill. Compatibility: - ServiceInfo.Methods: only original canonical names (clean metadata) - Registry / gRPC reflection: no duplicate entries - infoProxyInvoker: unaffected, lookups still use canonical names - Method-scoped config keys: unchanged, use canonical name (e.g. SayHello) - Java alias paths: routed correctly without changing invocation semantics Alias path note: methods.<MethodName>.* config keys use the canonical Go/proto name. Java-style aliases (sayHello) are routing-only and are not valid provider method-scoped config keys. Signed-off-by: MoChengqian <mochengqian@example.com>
1 parent ba17613 commit 03916a9

4 files changed

Lines changed: 313 additions & 124 deletions

File tree

protocol/triple/server.go

Lines changed: 120 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import (
4242
import (
4343
"dubbo.apache.org/dubbo-go/v3/common"
4444
"dubbo.apache.org/dubbo-go/v3/common/constant"
45+
"dubbo.apache.org/dubbo-go/v3/common/dubboutil"
4546
"dubbo.apache.org/dubbo-go/v3/global"
4647
"dubbo.apache.org/dubbo-go/v3/internal"
4748
"dubbo.apache.org/dubbo-go/v3/protocol/base"
@@ -320,109 +321,133 @@ func (s *Server) compatRegisterHandler(interfaceName string, svc dubbo3.Dubbo3Gr
320321
}
321322
}
322323

323-
// handleServiceWithInfo injects invoker and create handler based on ServiceInfo
324+
// handleServiceWithInfo injects invoker and creates handlers based on ServiceInfo.
325+
// Each method is registered under both its original procedure path and the
326+
// first-rune-swapped path, so Java clients (camelCase) and Go clients
327+
// (PascalCase) can both reach the same handler without polluting ServiceInfo.
324328
func (s *Server) handleServiceWithInfo(interfaceName string, invoker base.Invoker, info *common.ServiceInfo, opts ...tri.HandlerOption) {
325329
for _, method := range info.Methods {
326330
m := method
327331
procedure := joinProcedure(interfaceName, method.Name)
328-
switch m.Type {
329-
case constant.CallUnary:
330-
_ = s.triServer.RegisterUnaryHandler(
331-
procedure,
332-
m.ReqInitFunc,
333-
func(ctx context.Context, req *tri.Request) (*tri.Response, error) {
334-
var args []any
335-
if argsRaw, ok := req.Msg.([]any); ok {
336-
// non-idl mode, req.Msg consists of many arguments
337-
for _, argRaw := range argsRaw {
338-
// refer to createServiceInfoWithReflection, in ReqInitFunc, argRaw is a pointer to real arg.
339-
// so we have to invoke Elem to get the real arg.
340-
args = append(args, reflect.ValueOf(argRaw).Elem().Interface())
341-
}
342-
} else {
343-
// triple idl mode and old triple idl mode
344-
args = append(args, req.Msg)
345-
}
346-
attachments := generateAttachments(req.Header())
347-
// inject attachments
348-
ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
349-
invo := invocation.NewRPCInvocation(m.Name, args, attachments)
350-
res := invoker.Invoke(ctx, invo)
351-
// todo(DMwangnima): modify InfoInvoker to get a unified processing logic
352-
// please refer to server/InfoInvoker.Invoke()
353-
var triResp *tri.Response
354-
if existingResp, ok := res.Result().(*tri.Response); ok {
355-
triResp = existingResp
356-
} else {
357-
// please refer to proxy/proxy_factory/ProxyInvoker.Invoke
358-
triResp = tri.NewResponse([]any{res.Result()})
332+
s.registerMethodHandler(procedure, m, invoker, opts...)
333+
334+
// Also register the first-rune-swapped procedure so that Java clients
335+
// sending camelCase method names (e.g. getUser) reach Go handlers
336+
// registered as PascalCase (e.g. GetUser), and vice-versa.
337+
//
338+
// The original m (with the Go/proto name) is passed unchanged so that:
339+
// 1. infoProxyInvoker can look up the method by its canonical name.
340+
// 2. Method-scoped provider config ("methods.SayHello.*") continues
341+
// to apply; filters look up via invocation.MethodName(), which
342+
// must match the key written by server/action.go.
343+
//
344+
// Only the HTTP procedure path varies between the two registrations;
345+
// the invocation name and MethodInfo are shared.
346+
if swappedName := dubboutil.SwapCaseFirstRune(method.Name); joinProcedure(interfaceName, swappedName) != procedure {
347+
s.registerMethodHandler(joinProcedure(interfaceName, swappedName), m, invoker, opts...)
348+
}
349+
}
350+
}
351+
352+
// registerMethodHandler registers a single method handler for the given procedure path.
353+
func (s *Server) registerMethodHandler(procedure string, m common.MethodInfo, invoker base.Invoker, opts ...tri.HandlerOption) {
354+
switch m.Type {
355+
case constant.CallUnary:
356+
_ = s.triServer.RegisterUnaryHandler(
357+
procedure,
358+
m.ReqInitFunc,
359+
func(ctx context.Context, req *tri.Request) (*tri.Response, error) {
360+
var args []any
361+
if argsRaw, ok := req.Msg.([]any); ok {
362+
// non-idl mode, req.Msg consists of many arguments
363+
for _, argRaw := range argsRaw {
364+
// refer to createServiceInfoWithReflection, in ReqInitFunc, argRaw is a pointer to real arg.
365+
// so we have to invoke Elem to get the real arg.
366+
args = append(args, reflect.ValueOf(argRaw).Elem().Interface())
359367
}
360-
for k, v := range res.Attachments() {
361-
switch val := v.(type) {
362-
case string:
363-
tri.AppendToOutgoingContext(ctx, k, val)
364-
case []string:
365-
for _, v := range val {
366-
tri.AppendToOutgoingContext(ctx, k, v)
367-
}
368+
} else {
369+
// triple idl mode and old triple idl mode
370+
args = append(args, req.Msg)
371+
}
372+
attachments := generateAttachments(req.Header())
373+
// inject attachments
374+
ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
375+
invo := invocation.NewRPCInvocation(m.Name, args, attachments)
376+
res := invoker.Invoke(ctx, invo)
377+
// todo(DMwangnima): modify InfoInvoker to get a unified processing logic
378+
// please refer to server/InfoInvoker.Invoke()
379+
var triResp *tri.Response
380+
if existingResp, ok := res.Result().(*tri.Response); ok {
381+
triResp = existingResp
382+
} else {
383+
// please refer to proxy/proxy_factory/ProxyInvoker.Invoke
384+
triResp = tri.NewResponse([]any{res.Result()})
385+
}
386+
for k, v := range res.Attachments() {
387+
switch val := v.(type) {
388+
case string:
389+
tri.AppendToOutgoingContext(ctx, k, val)
390+
case []string:
391+
for _, v := range val {
392+
tri.AppendToOutgoingContext(ctx, k, v)
368393
}
369394
}
395+
}
396+
return triResp, res.Error()
397+
},
398+
opts...,
399+
)
400+
case constant.CallClientStream:
401+
_ = s.triServer.RegisterClientStreamHandler(
402+
procedure,
403+
func(ctx context.Context, stream *tri.ClientStream) (*tri.Response, error) {
404+
var args []any
405+
args = append(args, m.StreamInitFunc(stream))
406+
attachments := generateAttachments(stream.RequestHeader())
407+
// inject attachments
408+
ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
409+
invo := invocation.NewRPCInvocation(m.Name, args, attachments)
410+
res := invoker.Invoke(ctx, invo)
411+
if triResp, ok := res.Result().(*tri.Response); ok {
370412
return triResp, res.Error()
371-
},
372-
opts...,
373-
)
374-
case constant.CallClientStream:
375-
_ = s.triServer.RegisterClientStreamHandler(
376-
procedure,
377-
func(ctx context.Context, stream *tri.ClientStream) (*tri.Response, error) {
378-
var args []any
379-
args = append(args, m.StreamInitFunc(stream))
380-
attachments := generateAttachments(stream.RequestHeader())
381-
// inject attachments
382-
ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
383-
invo := invocation.NewRPCInvocation(m.Name, args, attachments)
384-
res := invoker.Invoke(ctx, invo)
385-
if triResp, ok := res.Result().(*tri.Response); ok {
386-
return triResp, res.Error()
387-
}
388-
// please refer to proxy/proxy_factory/ProxyInvoker.Invoke
389-
triResp := tri.NewResponse([]any{res.Result()})
390-
return triResp, res.Error()
391-
},
392-
opts...,
393-
)
394-
case constant.CallServerStream:
395-
_ = s.triServer.RegisterServerStreamHandler(
396-
procedure,
397-
m.ReqInitFunc,
398-
func(ctx context.Context, req *tri.Request, stream *tri.ServerStream) error {
399-
var args []any
400-
args = append(args, req.Msg, m.StreamInitFunc(stream))
401-
attachments := generateAttachments(req.Header())
402-
// inject attachments
403-
ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
404-
invo := invocation.NewRPCInvocation(m.Name, args, attachments)
405-
res := invoker.Invoke(ctx, invo)
406-
return res.Error()
407-
},
408-
opts...,
409-
)
410-
case constant.CallBidiStream:
411-
_ = s.triServer.RegisterBidiStreamHandler(
412-
procedure,
413-
func(ctx context.Context, stream *tri.BidiStream) error {
414-
var args []any
415-
args = append(args, m.StreamInitFunc(stream))
416-
attachments := generateAttachments(stream.RequestHeader())
417-
// inject attachments
418-
ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
419-
invo := invocation.NewRPCInvocation(m.Name, args, attachments)
420-
res := invoker.Invoke(ctx, invo)
421-
return res.Error()
422-
},
423-
opts...,
424-
)
425-
}
413+
}
414+
// please refer to proxy/proxy_factory/ProxyInvoker.Invoke
415+
triResp := tri.NewResponse([]any{res.Result()})
416+
return triResp, res.Error()
417+
},
418+
opts...,
419+
)
420+
case constant.CallServerStream:
421+
_ = s.triServer.RegisterServerStreamHandler(
422+
procedure,
423+
m.ReqInitFunc,
424+
func(ctx context.Context, req *tri.Request, stream *tri.ServerStream) error {
425+
var args []any
426+
args = append(args, req.Msg, m.StreamInitFunc(stream))
427+
attachments := generateAttachments(req.Header())
428+
// inject attachments
429+
ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
430+
invo := invocation.NewRPCInvocation(m.Name, args, attachments)
431+
res := invoker.Invoke(ctx, invo)
432+
return res.Error()
433+
},
434+
opts...,
435+
)
436+
case constant.CallBidiStream:
437+
_ = s.triServer.RegisterBidiStreamHandler(
438+
procedure,
439+
func(ctx context.Context, stream *tri.BidiStream) error {
440+
var args []any
441+
args = append(args, m.StreamInitFunc(stream))
442+
attachments := generateAttachments(stream.RequestHeader())
443+
// inject attachments
444+
ctx = context.WithValue(ctx, constant.AttachmentKey, attachments)
445+
invo := invocation.NewRPCInvocation(m.Name, args, attachments)
446+
res := invoker.Invoke(ctx, invo)
447+
return res.Error()
448+
},
449+
opts...,
450+
)
426451
}
427452
}
428453

protocol/triple/server_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
import (
3737
"dubbo.apache.org/dubbo-go/v3/common"
3838
"dubbo.apache.org/dubbo-go/v3/common/constant"
39+
"dubbo.apache.org/dubbo-go/v3/common/dubboutil"
3940
"dubbo.apache.org/dubbo-go/v3/global"
4041
)
4142

@@ -542,3 +543,128 @@ func Test_isReflectValueNil_UnsafePointer(t *testing.T) {
542543
assert.False(t, isReflectValueNil(v))
543544
})
544545
}
546+
547+
// TestHandleServiceWithInfo_DualProcedure verifies that handleServiceWithInfo
548+
// produces two distinct procedure paths for each method (original + swapped
549+
// first-rune case), enabling Go/Java cross-language interoperability without
550+
// polluting ServiceInfo.Methods.
551+
func TestHandleServiceWithInfo_DualProcedure(t *testing.T) {
552+
const interfaceName = "com.example.GreetService"
553+
554+
tests := []struct {
555+
methodName string
556+
wantOriginal string
557+
wantSwapped string
558+
distinctPaths bool
559+
}{
560+
{
561+
// Go PascalCase: Java sends "greet", Go registers "Greet"
562+
methodName: "Greet",
563+
wantOriginal: "/" + interfaceName + "/Greet",
564+
wantSwapped: "/" + interfaceName + "/greet",
565+
distinctPaths: true,
566+
},
567+
{
568+
// Java camelCase coming in: Go handler is "GetUser"
569+
methodName: "getUser",
570+
wantOriginal: "/" + interfaceName + "/getUser",
571+
wantSwapped: "/" + interfaceName + "/GetUser",
572+
distinctPaths: true,
573+
},
574+
{
575+
// Edge case: single lowercase rune
576+
methodName: "a",
577+
wantOriginal: "/" + interfaceName + "/a",
578+
wantSwapped: "/" + interfaceName + "/A",
579+
distinctPaths: true,
580+
},
581+
}
582+
583+
for _, tt := range tests {
584+
t.Run(tt.methodName, func(t *testing.T) {
585+
original := joinProcedure(interfaceName, tt.methodName)
586+
swapped := joinProcedure(interfaceName, dubboutil.SwapCaseFirstRune(tt.methodName))
587+
588+
assert.Equal(t, tt.wantOriginal, original)
589+
assert.Equal(t, tt.wantSwapped, swapped)
590+
if tt.distinctPaths {
591+
assert.NotEqual(t, original, swapped,
592+
"original and swapped procedures must be distinct so both get registered")
593+
}
594+
})
595+
}
596+
}
597+
598+
// TestHandleServiceWithInfo_AliasSharesMethodInfo verifies the contract of
599+
// handleServiceWithInfo's alias registration:
600+
//
601+
// - The alias path (e.g. /.../sayHello) and the original path (/.../SayHello)
602+
// share the same MethodInfo — including the same m.Name (Go/proto name).
603+
// - Only the HTTP procedure path differs; the invocation name is unchanged.
604+
//
605+
// This is required for two reasons:
606+
// 1. infoProxyInvoker builds its lookup table from ServiceInfo.Methods[i].Name
607+
// (proxy/proxy_factory/default.go:194-198). If the invocation carries the
608+
// alias name ("sayHello"), the lookup fails with "no match method for …".
609+
// 2. Method-scoped provider config is keyed as "methods."+v.Name+"." in
610+
// server/action.go:412. Filters look up via invocation.MethodName(), so
611+
// the invocation must carry the Go name for config to apply correctly.
612+
func TestHandleServiceWithInfo_AliasSharesMethodInfo(t *testing.T) {
613+
const interfaceName = "com.example.GreetService"
614+
615+
cases := []struct {
616+
goName string // canonical name in ServiceInfo.Methods and config keys
617+
aliasName string // first-rune-swapped name used only as URL path
618+
}{
619+
{"SayHello", "sayHello"},
620+
{"GetUser", "getUser"},
621+
{"greet", "Greet"},
622+
}
623+
624+
for _, c := range cases {
625+
t.Run(c.goName, func(t *testing.T) {
626+
m := common.MethodInfo{Name: c.goName, Type: constant.CallUnary}
627+
628+
// Simulate what handleServiceWithInfo does for the alias path:
629+
// only the procedure path changes; m is passed unchanged.
630+
originalProcedure := joinProcedure(interfaceName, m.Name)
631+
aliasProcedure := joinProcedure(interfaceName, dubboutil.SwapCaseFirstRune(m.Name))
632+
633+
assert.NotEqual(t, originalProcedure, aliasProcedure,
634+
"alias procedure path must differ from the original")
635+
assert.Equal(t, c.goName, m.Name,
636+
"MethodInfo.Name must remain the Go/proto name so infoProxyInvoker and config lookups succeed")
637+
assert.Equal(t, c.aliasName, dubboutil.SwapCaseFirstRune(m.Name),
638+
"SwapCaseFirstRune produces the alias URL segment, not the invocation name")
639+
})
640+
}
641+
}
642+
643+
// TestHandleServiceWithInfo_SaveServiceInfo_OnlyOriginalMethods verifies that
644+
// saveServiceInfo (called after handleServiceWithInfo) only records the original
645+
// method names – not swapped-case aliases – so registry/gRPC-reflection
646+
// metadata stays clean.
647+
func TestHandleServiceWithInfo_SaveServiceInfo_OnlyOriginalMethods(t *testing.T) {
648+
server := NewServer(nil)
649+
info := &common.ServiceInfo{
650+
Methods: []common.MethodInfo{
651+
{Name: "GetUser", Type: constant.CallUnary},
652+
{Name: "ListUsers", Type: constant.CallServerStream},
653+
},
654+
}
655+
server.saveServiceInfo("com.example.UserService", info)
656+
657+
svcInfo := server.GetServiceInfo()
658+
svc, ok := svcInfo["com.example.UserService"]
659+
assert.True(t, ok)
660+
// Only the two original methods; no "getUser" / "listUsers" aliases.
661+
assert.Len(t, svc.Methods, 2)
662+
names := make([]string, 0, len(svc.Methods))
663+
for _, m := range svc.Methods {
664+
names = append(names, m.Name)
665+
}
666+
assert.Contains(t, names, "GetUser")
667+
assert.Contains(t, names, "ListUsers")
668+
assert.NotContains(t, names, "getUser")
669+
assert.NotContains(t, names, "listUsers")
670+
}

0 commit comments

Comments
 (0)