CAICAIIs opened a new issue, #3751:
URL: https://github.com/apache/dubbo-go/issues/3751

   ### What problem would this solve?
   
   A client without generated stubs — a gateway, a console, a test harness — 
cannot call a typed stub, so it depends on Dubbo's generic contract, whose 
triple wire form is the `$invoke` procedure. Triple dispatches by procedure 
path, so the provider has to have `/<interface>/$invoke` registered. Plain HTTP 
with protobuf JSON can also reach an IDL service, but that is a different 
integration path: it bypasses the dubbo-go client, so it uses neither the 
configured generic mode nor the reference's attachments, cluster and retry 
policies.
   
   dubbo-go registers `$invoke` for services exported in non-IDL mode only. A 
service exported with an IDL `ServiceInfo` registers just its concrete methods, 
so a generic call is rejected by the procedure router with `404 Not Found` and 
never reaches the provider filter chain that would have dispatched it.
   
   The asymmetry is the defect: `client.NewGenericService` always builds a 
non-IDL consumer, while whether `/<interface>/$invoke` exists is decided by the 
provider's export mode.
   
   Downstream: apache/dubbo-go-pixiu#918 asks whether the gateway still needs a 
descriptor-based IDL path.
   
   ### Current behavior (develop @ 302aa0e8)
   
   1. The consumer always sends `$invoke`. `GenericService` declares its entry 
point as `Invoke func(...) (any, error)` with the tag `dubbo:"$invoke"` 
(`filter/generic/service.go:35`), and the proxy turns that tag into the 
invocation name (`proxy/proxy.go:255`). `client.NewGenericService` 
(`client/client.go:161`) additionally forces `WithIDL(constant.NONIDL)`, 
`WithGeneric()` and hessian2, and the consumer filter recognizes the call by 
name and arity (`filter/generic/util.go:41`). `constant.Generic = "$invoke"` 
(`common/constant/default.go:58`). A typed call made through a generic 
reference is rewritten to the same name (`filter/generic/filter.go:112`).
   
   2. Triple routes by procedure path, and an unregistered procedure is an HTTP 
404 (`protocol/triple/triple_protocol/method_route_mux.go:86,104`). The path is 
joined on the client side from the reference interface and the method name 
(`protocol/triple/triple_protocol/client.go:356`) and mirrored on the server by 
`joinProcedure` (`protocol/triple/common.go:26`).
   
   3. `$invoke` is registered on the non-IDL branch only. 
`registerServiceHandlers` (`protocol/triple/server.go:241`) has three branches:
   
   ```go
   if info != nil {                       // IDL export: only the methods 
carried by ServiceInfo
   } else if IDLMode == constant.NONIDL { // non-IDL export: reflection plus 
$invoke
   }
   ```
   
   `buildGenericMethodInfo()` is appended inside 
`createServiceInfoWithReflection` (`protocol/triple/server.go:765`, definition 
at `:831`), so only non-IDL exports get `/<interface>/$invoke`, and 
`handleServiceWithInfo` (`:521`) never creates a handler for it. The third 
branch is the legacy compat path (`info == nil` with IDL mode) and is out of 
scope here.
   
   4. The provider side is otherwise ready. `generic_service` is part of 
`DefaultServiceFilters` (`common/constant/default.go:69`), and 
`genericServiceFilter.Invoke` (`filter/generic/service_filter.go:67`) resolves 
the concrete method through `common.ServiceMap` (`:93`). IDL services are 
registered there (`server/action.go:164`, with the interface name taken from 
`info.InterfaceName` at `server/server.go:182`), and for services implemented 
with generated protobuf parameters their argument types are protobuf messages, 
which is what the `protobuf-json` generalizer expects 
(`filter/generic/generalizer/protobuf_json.go:62`).
   
   5. Since no invocation is ever created, none of that runs. The caller only 
sees a bare 404, with no hint that the provider lacks a generic entry point.
   
   ### Proposed solution (optional)
   
   Register the `$invoke` procedure for the IDL branch as well, next to the 
concrete methods. `buildGenericMethodInfo()` already carries the `ReqInitFunc` 
(`protocol/triple/server.go:831`) that declares the destination shape the 
wrapper codec decodes into (`protocol/triple/triple_protocol/codec.go:319`, 
slice branch `:321`, inner codec selected from the wrapper's `serializeType` at 
`:331`), so this is a registration change rather than a new conversion layer.
   
   One implementation choice to settle in review: append the method to 
`info.Methods` for symmetry with the non-IDL branch, or register the procedure 
directly through `registerMethodHandler(joinProcedure(intfName, 
constant.Generic), buildGenericMethodInfo(), invoker, opts...)` so the 
`ServiceInfo` handed to `saveServiceInfo` stays untouched. Non-IDL exports 
already put `$invoke` into that list (`:765` → `:271` → `:661`, which also 
drives the OpenAPI registration at `:690`), so appending would add `$invoke` to 
the reflection and OpenAPI metadata of IDL services as well, while direct 
registration leaves that metadata alone. 
`TestHandleServiceWithInfoSaveServiceInfoOnlyOriginalMethods` 
(`protocol/triple/server_test.go:554`) asserts that what `saveServiceInfo` 
records stays close to the original method list, which is an argument for 
direct registration.
   
   A second question: register the entry for every IDL export, or make it 
opt-in through a server option? The non-IDL branch registers it unconditionally 
today.
   
   Tests to add: a unit assertion in `protocol/triple/server_test.go`, next to 
`TestHandleServiceWithInfoSaveServiceInfoOnlyOriginalMethods` (`:554`), that an 
IDL export exposes the `$invoke` procedure. The end-to-end case with an IDL 
provider and a generic client lives in dubbo-go-samples and would be tracked 
separately.
   
   ### Scope
   
   - unary only (`buildGenericMethodInfo` registers `CallUnary`); streaming 
generic invocation is out of scope;
   - generic modes covered: `protobuf-json`; map mode needs the check described 
below;
   - no client-side API change, no change to the non-IDL path.
   
   ### Acceptance
   
   - An IDL triple provider exported through `server.Register(...)` can be 
invoked with `NewGenericService(iface, WithProtocolTriple(), 
WithGenericType("protobuf-json"), WithURL("tri://127.0.0.1:50052"))`.
   - Map mode (`WithGenericType("true")`) against the same provider is expected 
to work, to be confirmed while implementing: `MapGeneralizer.Realize` decodes 
with mapstructure using the `m` tag (`filter/generic/generalizer/map.go:79`), 
while generated protobuf structs carry `protobuf` and `json` tags, so 
`repeated`, nested and enum fields need checking.
   - A provider exported through `RegisterService` (non-IDL) keeps working 
unchanged.
   - A generic call naming a method the service does not expose still returns 
the `"<method>" method is not found` error from `genericServiceFilter` 
(`filter/generic/service_filter.go:97`) instead of a 404, with the name given 
as registered in `common.ServiceMap` (canonical name or the first-rune-swapped 
alias added by `suitableMethods`, `common/rpc_service.go:575`).
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to