Repository navigation
feat(pubsub): promote BulkPublishEventAlpha1 to stable BulkPublishEvent - #856
Conversation
Dapr promoted the bulk publish API out of alpha in runtime 1.17, but the JS
SDK still targeted the alpha1 variants over both transports. Move to the
stable API while keeping older sidecars working.
- gRPC: call BulkPublishEvent, falling back to BulkPublishEventAlpha1 when
the sidecar does not serve it.
- HTTP: call v1.0/publish/bulk/{pubsubName}/{topic}, falling back to
v1.0-alpha1 on a 404. A 404 also signals an unknown pub/sub component, so
that downgrade is only remembered once the alpha1 endpoint has answered.
- Vendored proto: backport the stable BulkPublishEvent RPC and mark the
alpha1 RPC deprecated, matching upstream dapr/dapr, and regenerate the
affected stubs.
Detecting an old sidecar over gRPC is not what you would expect. Dapr
installs a catch-all handler that forwards unrecognised methods to service
invocation, so a pre-1.17 sidecar answers UNKNOWN with "failed to proxy
request" rather than UNIMPLEMENTED. Both shapes are recognised, with the
UNKNOWN match narrowed to the proxy signature so a genuine publish error is
never retried in a way that could publish the same batch twice.
The public surface is unchanged: publishBulk keeps the same name, parameters
and return type. The fallback is remembered per client, so a pre-1.17 sidecar
costs at most one extra round trip.
Verified with test:e2e:common:client against Dapr 1.16.12 and 1.17.14, 44/44
on both.
Fixes dapr#786
Signed-off-by: Mark Gardner <mark@markjgardner.com>
|
@WhitWaldo Hope you don't mind me stealing your issue but #786 looked like an easy starter issue so I gave it a stab. Let me know what you think. |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Concurrent initial publishes can each perform the failed capability probe, violating the stated one-probe-per-client guarantee.
Review effort: Balanced
Findings: 2
Open (2)
What changed in this PR
Promotes bulk publish to Dapr’s stable API while retaining transparent compatibility with older sidecars.
Changes:
- Adds stable gRPC and HTTP bulk-publish paths with alpha1 fallback.
- Backports the stable RPC and updates generated stubs.
- Adds transport-specific tests and documentation.
| File | Description |
|---|---|
CHANGELOG.md |
Documents stable API promotion and fallback behavior. |
dapr/proto/runtime/v1/dapr.proto |
Adds the stable RPC and deprecates alpha1. |
src/interfaces/Client/IClientPubSub.ts |
Documents compatibility behavior. |
src/implementation/Client/GRPCClient/pubsub.ts |
Implements stable-first gRPC publishing. |
src/implementation/Client/HTTPClient/pubsub.ts |
Implements stable-first HTTP publishing. |
src/proto/dapr/proto/runtime/v1/dapr_pb.js |
Updates the generated descriptor. |
src/proto/dapr/proto/runtime/v1/dapr_pb.d.ts |
Adds stable RPC typings. |
src/proto/dapr/proto/runtime/v1/dapr_connect.js |
Adds stable ConnectRPC metadata. |
src/proto/dapr/proto/runtime/v1/dapr_connect.d.ts |
Adds stable ConnectRPC typings. |
test/unit/protocols/grpc/pubsub.test.ts |
Tests gRPC selection and fallback. |
test/unit/protocols/http/pubsub.test.ts |
Tests HTTP selection and fallback. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #856 +/- ##
============================================
- Coverage 100.00% 47.29% -52.71%
============================================
Files 1 135 +134
Lines 6 4068 +4062
Branches 1 764 +763
============================================
+ Hits 6 1924 +1918
- Misses 0 2003 +2003
- Partials 0 141 +141
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
@markjgardner Could you please address Copilot's feedback? |
…urrent calls Concurrent first publishBulk calls could each probe the stable bulk publish API before the fallback flag was set, so a pre-1.17 sidecar cost one failed round trip and one warning per in-flight call rather than per client. The first bulk publish on a client now doubles as the capability probe. Calls that start while it is in flight wait for its outcome, then publish their own batch through the selected API. The shared promise never rejects, so a failing first publish does not fail the calls waiting on it, and the fallback warning is only logged on the transition. Over HTTP, a stable 404 followed by any non-404 answer from the alpha1 endpoint, such as a 500 for a partially failed batch, now also confirms the downgrade. Dapr serves both versions from the same handler, so a sidecar with the stable endpoint answers alpha1 with the same 404 when the pub/sub component is unknown. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Mark Gardner <mark@markjgardner.com>
Avoid probing the stable HTTP bulk-publish route with a POST on older runtimes, where it can be routed as a regular publish. Read the sidecar runtime version first and use the alpha1 API before Dapr 1.17. Share the version lookup across concurrent publishes and fail closed when metadata cannot identify the runtime. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Mark Gardner <mark@markjgardner.com>
|
@markjgardner I've added a bunch of PRs this morning - could you please resolve the merge conflicts? Once tests pass, we should be good to go! |
Resolve the upstream changes to the gRPC and HTTP pub/sub clients and generated protobuf files while preserving stable bulk publish behavior and HTTP runtime-version routing. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Signed-off-by: Mark Gardner <mark@markjgardner.com>
|
Resolved the merge conflicts in 76ac4c9 by merging the latest upstream/main. The full unit suite passes (52 suites, 814 tests); TypeScript, ESLint, Prettier, and diff checks also pass. GitHub now reports the PR as mergeable. DCO is passing; other CI checks have not appeared yet. |
|
@markjgardner Thank you very much for taking the time to put this PR together and contribute to this project. I certainly appreciate it! |
|
@holopin-bot @markjgardner sdk-badge Thank you! Here's a digital badge as a small token of appreciation. |
|
Congratulations @markjgardner, the maintainer of this repository has issued you a badge! Here it is: https://holopin.io/claim/cmvgexvd80007gz044hsffer1 This badge can only be claimed by you, so make sure that your GitHub account is linked to your Holopin account. You can manage those preferences here: https://holopin.io/account. |


Description
Dapr promoted the bulk publish API out of alpha in runtime 1.17, but the JS SDK still targeted the alpha1 variants on both transports. This moves
client.pubsub.publishBulk(...)onto the stable API without dropping support for the runtimes people are actually on.Approach: stable first, with a transparent fallback.
BulkPublishEvent, retryingBulkPublishEventAlpha1when the sidecar does not serve it.v1.0/publish/bulk/{pubsubName}/{topic}, retryingv1.0-alpha1on a 404.BulkPublishEventRPC and marks alpha1deprecated, matching upstream dapr/dapr, and the affected stubs are regenerated.The public surface does not change.
publishBulkkeeps the same name, parameters and return type, so no consumer code needs updating. The downgrade is remembered per client, and bulk publishes that start while the first one is still in flight wait for its outcome instead of each probing the sidecar, so a pre-1.17 sidecar normally costs one extra round trip and one warning per client rather than one per call.Detecting an old sidecar is not what you would expect
This is the one genuinely non-obvious part of the change, so it is worth calling out.
A sidecar that predates the RPC should answer
UNIMPLEMENTED. It does not. Dapr installs a catch-all gRPC handler that forwards unrecognised methods to service invocation, so a pre-1.17 sidecar instead returnsUNKNOWNwith:An
UNIMPLEMENTED-only check therefore never matches, and bulk publish over gRPC breaks on every runtime older than 1.17. This was caught by running the e2e suite against 1.16.12; unit tests with a mocked client cannot surface it.Both shapes are recognised. The
UNKNOWNbranch is deliberately narrowed to the proxy-failure signature rather than treating everyUNKNOWNas a missing RPC, because a proxy failure proves the sidecar never reached the publish path and a retry is safe, whereas a blanket match could retry a batch the sidecar had already accepted and publish it twice.The HTTP side has a similar ambiguity: 404 is also how the sidecar reports an unknown pub/sub component. So the HTTP downgrade is only remembered once the alpha1 endpoint has actually answered, which is the only real evidence the runtime predates 1.17. Any answer other than a 404 counts, including a 500 for a partially failed batch, because Dapr serves both versions from the same handler, so a sidecar that has the stable endpoint would answer alpha1 with the same 404.
Heads-up for maintainers: this is sidecar behaviour rather than client behaviour, so the equivalent
UNIMPLEMENTEDchecks in the Go, Python and Java SDKs look like they have the same latent gap.Why a fallback rather than bumping the pinned runtime
A hard switch is not really available here. The stable RPC is absent from the pinned v1.16.0 proto, so it would require bumping the pin, and Dapr 1.17 split
dapr.protointo 14 files. That relocates message types out ofdapr_pbinto new modules and breaks imports across the whole SDK, which is a much larger change than this issue. Separately, e2e defaults toDAPR_RUNTIME_VER=1.16.12, so a hard switch would fail CI, and 1.16.x is still actively patched. Since 1.17 and 1.18 both still register alpha1 on both transports, the fallback never becomes dead code that silently breaks.Please look closely at the vendored proto
dapr/proto/runtime/v1/dapr.protois hand-edited here. The sanctioned path is bumpingDAPR_VERSIONinscripts/fetch-proto.js, which is what #531 did for workflow, and I did not take it because of the 14-file split described above. The backported RPC is byte-for-byte identical to dapr/dapr, includingoption deprecated = trueand the surrounding comments, so a later fetch at v1.17+ converges rather than conflicts. If you would rather see the pin bump land first as its own PR, that is a reasonable ask and I am happy to rework this on top of it.A note on the generated stubs
Flagging this in case it is useful rather than as a complaint, since it shaped how I generated the delta.
The committed stubs under
src/protowere last generated on 2026-04-16 by the buf migration in #752, using protoc-gen-es v2.11.0 in ESM form. #826 then switchedbuf.gen.yamltojs_import_style=legacy_commonjson 2026-05-26 without regenerating them. The result is that runningnpx buf generateon current main rewrites every stub from ESM to CommonJS, around 750 lines. That is your intended direction rather than churn, but it does mean any routine regeneration during an unrelated PR carries the migration with it.So I generated the bulk publish delta in the existing ESM style, which keeps the codegen diff here to 43 lines across four files and keeps this PR about pubsub. Note the proto change is the load-bearing part either way: CI runs
buf generateduring prebuild, so the stable RPC is picked up from the.protoregardless of the committed stub format, which I verified with a localnpm run build.Happy to regenerate as CommonJS in this PR instead, or to send the catch-up as its own PR, whichever you prefer.
Verification
E2E run through testcontainers, the same harness
test-e2e-testcontainers.ymluses, on c9d2830:Unit tests (
npm run test:unit:all): 339 passing across 31 suites, including 22 new ones, 11 for gRPC and 11 for HTTP. Beyond the happy paths they cover the proxy-failure shape falling back, an unrelatedUNKNOWNnot falling back, a 404 from both HTTP endpoints being treated as an unknown component rather than an old runtime, and the downgrade being probed only once per client, including when several publishes start concurrently. They also check that a failing first publish does not fail the calls waiting on it, that the fallback warning is logged once, and that an alpha1 partial failure over HTTP confirms the downgrade.42f2226 addresses the review feedback on concurrent probes. It is covered by the unit tests above, but the E2E run has not been repeated on it.
Total diff is 11 files, +756 / -16.
Issue reference
We strive to have all PR being opened based on an issue, where the problem or feature have been discussed prior to implementation.
Please reference the issue this PR will close: #786
Checklist
Please make sure you've completed the relevant tasks for this PR, out of the following list: