hashgraph / hashgraph/pbj

Allow attaching Filter-style hooks to PbjRouting (mirror Helidon's HttpRouting.Builder.addFilter)

Open
#937 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Java
Stars
44
Forks
15
Avg merge
1d 15h
Merged PRs (30d)
12

Description

## Story Form

As a consumer of `pbj-grpc-helidon`
I want to attach a `Filter`-like hook to `PbjRouting.Builder`, the same way Helidon's own `HttpRouting.Builder.addFilter(Filter)` works for plain HTTP
So that I can apply a cross-cutting concern (admission control, auth, logging) uniformly to every gRPC service registered on that builder, at registration time, without changing any service's own implementation

## Technical Notes

**The gap.** `PbjRouting.Builder` (`pbj-core/pbj-grpc-helidon/src/main/java/com/hedera/pbj/grpc/helidon/PbjRouting.java`,
tag `v0.15.10`) exposes exactly two public methods: `.service(ServiceInterface)` and `.service(ServiceInterface, PbjGrpcServiceConfig)`. There's no way to attach a hook that runs for every registered service uniformly. The only way to add cross-cutting behavior today is wrapping each `ServiceInterface` individually before registration, one decorator per service.

Helidon itself solves exactly this for plain HTTP via `io.helidon.webserver.http.HttpRouting.Builder.addFilter(Filter)`: a `Filter` receives a `FilterChain` plus the request/response, and either calls `chain.proceed()` to continue or doesn't, rejecting it. This is the same chain-of-responsibility shape most webserver frameworks (and grpc-java's own `ServerInterceptor`) use for exactly this class of problem.

**Why `HttpRouting`'s existing `Filter` doesn't already cover `PbjRouting` traffic.** `PbjRouting` is a separate `Routing` implementation, dispatched to via Helidon's `Http2SubProtocolSelector` SPI, not via `HttpRouting`.
Confirmed against the source at the pinned tag: `PbjProtocolSelector.subProtocol(...)` claims every HTTP/2 `POST`
unconditionally (`PbjProtocolSelector.java:63-112`), and Helidon's `Http2ServerStream.handle()` dispatches to whichever handler claims a stream first — `HttpRouting`'s filter chain only runs for streams nothing else claimed.
A `Filter` registered on `HttpRouting` never sees `PbjRouting` traffic; the two are separate, mutually-exclusive dispatch paths on the same socket, not layered one on top of the other.

*(Note: this is not a request for `io.grpc.ServerInterceptor` support — `pbj-grpc-helidon` doesn't run on grpc-java's server runtime at all, so that construct doesn't apply here regardless. The ask is specifically about Helidon's own `Filter`/`FilterChain` shape.)*

**Why this is achievable inside PBJ alone, no Helidon change needed.** Once `PbjProtocolSelector` claims a stream, every subsequent step — `PbjRouting.findRoute(...)`, `PbjMethodRoute`, `PbjProtocolHandler` — is entirely PBJ's own code, with no further calls back into Helidon. PBJ already has full, exclusive ownership of the exact point a `Filter`-style hook would need to run.

Also checked: this gap is still open as of the current `v0.16.0` tag (empty diff against `v0.15.10` for `pbj-core/pbj-grpc-helidon/src/main/java`), so it isn't something already resolved by upgrading.

### What we're asking for

Roughly:
1. `PbjRouting.Builder` gains something like `.addFilter(Filter filter)` — preferably reusing Helidon's own `Filter` type directly, or a PBJ-defined equivalent if that's a much better fit for PBJ's `Pipeline`-based model — applied to every service subsequently registered on that routing builder. The direct analog of `HttpRouting.Builder.addFilter(...)`.

**How we're using the wrapping approach today, for context.** hiero-block-node wraps each throttled `ServiceInterface` individually at registration time (`ServiceBuilderImpl.registerGrpcService`, `block-node/app` module) — see hiero-ledger/hiero-block-node#3529 for the full design and discussion. It works, but every registration call site has to know it's wrapping a decorator rather than the raw service, and there's no single place to apply the same behavior to every service in one motion.
A second use case (authentication) was also raised independently in that same PR thread as something this mechanism would be needed for.

#### `RoutingRequest` and `RoutingResponse` minimum implementation
Both `RoutingRequest` and `RoutingResponse` are large interfaces that accept and return several large and complex objects. To reduce unnecessary effort, we've defined a minimum viable implementation of those two interfaces.

##### Required elements
* For `RoutingRequest` as much as we can get, but at minimum (note,
returned objects can be read-only.
* authority, path, id, headers, requestedUri, isSecure, proxyProtocolData,
remotePeer
* For `RoutingResponse`, only status (to set return status if not proceeding).

##### Future Enhancement (currently "nice to have")
* RoutingRequest: prologue, localPeer, security, socketId, severSocketId, listenerContext,
matchingPattern, content.
* RoutingResponse: Almost everything.

### Not asking for

- Any change to PBJ's `Pipeline`/`ServiceInterface` contract (this is additive).
- Any specific policy (rate limiting, auth) built into PBJ itself(just the attachment point).
- Any change to Helidon, or any dependency on one (see "achievable inside PBJ alone" above).

Contributor guide

Open the contributing guide

Research direction

Start with pbj-core/pbj-grpc-helidon/src/main/java/com/hedera/pbj/grpc/helidon/PbjRouting.java, especially its Builder and findRoute path, then trace PbjMethodRoute and PbjProtocolHandler. Review PbjProtocolSelector.java:63-112 for the dispatch boundary. Done means a builder-level Filter-style hook applies to subsequently registered services, supports the stated minimum RoutingRequest and RoutingResponse elements, and preserves the existing service contract.

Written by the indexing model from the issue text.

Assessment

Tech stack
java
Domain
api, backend
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.