Summary
A server that wants to support both gRPC and protobuf POST over HTTP on the same path cannot do so — WebActionsServlet throws IllegalArgumentException at startup due to duplicate JSON variant routing.
Problem
WebActionFactory.transformActionIntoJson() automatically creates a JSON variant of every protobuf-based WebAction:
@WireRpc actions: JSON variant changes dispatch from GRPC to POST with JSON media types.
@Post + @RequestContentType(APPLICATION_PROTOBUF) actions: JSON variant replaces protobuf media types with JSON equivalents.
Since @WireRpc and @RequestContentType are mutually exclusive on the same function (enforced in Actions.kt), supporting both gRPC and protobuf POST requires two separate WebAction classes. Each independently produces a JSON variant, and the two JSON variants have identical routing:
| Action |
Dispatch |
Accepted Media |
Response Content Type |
| gRPC original |
GRPC |
application/grpc |
application/grpc |
| gRPC JSON variant |
POST |
application/json |
application/json;charset=utf-8 |
| Protobuf POST original |
POST |
application/x-protobuf |
application/x-protobuf |
| Protobuf POST JSON variant |
POST |
application/json |
application/json;charset=utf-8 |
WebActionsServlet's init block checks all bound actions pairwise via BoundAction.hasIdenticalRouting(), finds the two JSON variants identical, and throws:
IllegalArgumentException: Actions [MyGrpcAction, MyProtobufPostAction] have identical routing annotations.
Why existing deduplication doesn't help
collectBoundActions() deduplicates with actions.distinctBy { it.action }, but this only operates within a single WebAction class. It does not deduplicate across separate classes — each class goes through newBoundAction() independently from the WebActionsServlet init block.
No workaround available
Attempt: @WireRpc + @Post without @RequestContentType
Putting both annotations on one function avoids the duplicate routing error (the @Post variant defaults to MediaRange.ALL_MEDIA, so no JSON variant is created for it). However, without @ResponseContentType, the action's responseContentType is null, and ResponseBodyMarshallerFactory cannot find a marshaller for a proto return type:
IllegalArgumentException: no marshaller for null as HelloReply
at ResponseBodyMarshallerFactory.genericMarshallerFor(ResponseBodyMarshallerFactory.kt:33)
at ResponseBodyMarshallerFactory.create(ResponseBodyMarshallerFactory.kt:20)
at ResponseBodyFeatureBinding$Factory.create(ResponseBodyFeatureBinding.kt:122)
Adding @ResponseContentType(APPLICATION_PROTOBUF) fixes the marshaller but re-triggers transformActionIntoJson creating a JSON variant — bringing back the original duplicate routing error.
Prior art: Armeria unframed requests
Armeria handles this with enableUnframedRequests(true) on a GrpcService. A single gRPC service definition automatically supports gRPC, gRPC-Web, JSON POST (application/json), and protobuf POST (application/protobuf) — no separate actions, no variant collisions.
This matters for servers migrating from Armeria to Misk: Armeria clients using protobuf POST (unframed) to call a service will break after that service migrates to Misk, since Misk can't register both gRPC and protobuf POST on the same path.
Possible fixes
-
Cross-class JSON variant deduplication: Move deduplication from collectBoundActions() (per-class) to WebActionsServlet's init block (global). When two bound actions have identical routing, keep one and discard the duplicate instead of throwing.
-
Allow suppressing JSON variant creation: Add an annotation (e.g. @NoJsonVariant) that tells transformActionIntoJson to skip creating a JSON variant for a specific action.
-
Lift the @WireRpc + @RequestContentType restriction: Allow a single function to carry both annotations so one WebAction class can serve gRPC and protobuf POST, with the per-class distinctBy deduplication handling the single JSON variant.
Relevant code
WebActionFactory.transformActionIntoJson() — misk/src/main/kotlin/misk/web/actions/WebActionFactory.kt
WebActionsServlet init block — misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.kt
BoundAction.hasIdenticalRouting() — misk/src/main/kotlin/misk/web/BoundAction.kt
KFunction.asAction() — misk/src/main/kotlin/misk/Actions.kt
ResponseBodyMarshallerFactory — misk/src/main/kotlin/misk/web/interceptors/ResponseBodyMarshallerFactory.kt
Summary
A server that wants to support both gRPC and protobuf POST over HTTP on the same path cannot do so —
WebActionsServletthrowsIllegalArgumentExceptionat startup due to duplicate JSON variant routing.Problem
WebActionFactory.transformActionIntoJson()automatically creates a JSON variant of every protobuf-basedWebAction:@WireRpcactions: JSON variant changes dispatch fromGRPCtoPOSTwith JSON media types.@Post+@RequestContentType(APPLICATION_PROTOBUF)actions: JSON variant replaces protobuf media types with JSON equivalents.Since
@WireRpcand@RequestContentTypeare mutually exclusive on the same function (enforced inActions.kt), supporting both gRPC and protobuf POST requires two separateWebActionclasses. Each independently produces a JSON variant, and the two JSON variants have identical routing:GRPCapplication/grpcapplication/grpcPOSTapplication/jsonapplication/json;charset=utf-8POSTapplication/x-protobufapplication/x-protobufPOSTapplication/jsonapplication/json;charset=utf-8WebActionsServlet's init block checks all bound actions pairwise viaBoundAction.hasIdenticalRouting(), finds the two JSON variants identical, and throws:Why existing deduplication doesn't help
collectBoundActions()deduplicates withactions.distinctBy { it.action }, but this only operates within a singleWebActionclass. It does not deduplicate across separate classes — each class goes throughnewBoundAction()independently from theWebActionsServletinit block.No workaround available
Attempt:
@WireRpc+@Postwithout@RequestContentTypePutting both annotations on one function avoids the duplicate routing error (the
@Postvariant defaults toMediaRange.ALL_MEDIA, so no JSON variant is created for it). However, without@ResponseContentType, the action'sresponseContentTypeisnull, andResponseBodyMarshallerFactorycannot find a marshaller for a proto return type:Adding
@ResponseContentType(APPLICATION_PROTOBUF)fixes the marshaller but re-triggerstransformActionIntoJsoncreating a JSON variant — bringing back the original duplicate routing error.Prior art: Armeria unframed requests
Armeria handles this with
enableUnframedRequests(true)on aGrpcService. A single gRPC service definition automatically supports gRPC, gRPC-Web, JSON POST (application/json), and protobuf POST (application/protobuf) — no separate actions, no variant collisions.This matters for servers migrating from Armeria to Misk: Armeria clients using protobuf POST (unframed) to call a service will break after that service migrates to Misk, since Misk can't register both gRPC and protobuf POST on the same path.
Possible fixes
Cross-class JSON variant deduplication: Move deduplication from
collectBoundActions()(per-class) toWebActionsServlet's init block (global). When two bound actions have identical routing, keep one and discard the duplicate instead of throwing.Allow suppressing JSON variant creation: Add an annotation (e.g.
@NoJsonVariant) that tellstransformActionIntoJsonto skip creating a JSON variant for a specific action.Lift the
@WireRpc+@RequestContentTyperestriction: Allow a single function to carry both annotations so oneWebActionclass can serve gRPC and protobuf POST, with the per-classdistinctBydeduplication handling the single JSON variant.Relevant code
WebActionFactory.transformActionIntoJson()—misk/src/main/kotlin/misk/web/actions/WebActionFactory.ktWebActionsServletinit block —misk/src/main/kotlin/misk/web/jetty/WebActionsServlet.ktBoundAction.hasIdenticalRouting()—misk/src/main/kotlin/misk/web/BoundAction.ktKFunction.asAction()—misk/src/main/kotlin/misk/Actions.ktResponseBodyMarshallerFactory—misk/src/main/kotlin/misk/web/interceptors/ResponseBodyMarshallerFactory.kt