Skip to content

feat(pubsub): promote BulkPublishEventAlpha1 to stable BulkPublishEvent - #856

Merged
WhitWaldo merged 9 commits into
dapr:mainfrom
markjgardner:main
Oct 19, 2026
Merged

WhitWaldo merged 9 commits into
dapr:mainfrom
markjgardner:main

Conversation

@markjgardner

@markjgardner markjgardner commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

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.

  • gRPC calls BulkPublishEvent, retrying BulkPublishEventAlpha1 when the sidecar does not serve it.
  • HTTP calls v1.0/publish/bulk/{pubsubName}/{topic}, retrying v1.0-alpha1 on a 404.
  • The vendored proto backports the stable BulkPublishEvent RPC and marks alpha1 deprecated, matching upstream dapr/dapr, and the affected stubs are regenerated.

The public surface does not change. publishBulk keeps 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 returns UNKNOWN with:

failed to proxy request: required metadata dapr-callee-app-id or dapr-app-id not found

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 UNKNOWN branch is deliberately narrowed to the proxy-failure signature rather than treating every UNKNOWN as 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 UNIMPLEMENTED checks 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.proto into 14 files. That relocates message types out of dapr_pb into new modules and breaks imports across the whole SDK, which is a much larger change than this issue. Separately, e2e defaults to DAPR_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.proto is hand-edited here. The sanctioned path is bumping DAPR_VERSION in scripts/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, including option deprecated = true and 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/proto were last generated on 2026-04-16 by the buf migration in #752, using protoc-gen-es v2.11.0 in ESM form. #826 then switched buf.gen.yaml to js_import_style=legacy_commonjs on 2026-05-26 without regenerating them. The result is that running npx buf generate on 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 generate during prebuild, so the stable RPC is picked up from the .proto regardless of the committed stub format, which I verified with a local npm 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.yml uses, on c9d2830:

Runtime Result
1.16.12 (pre-stable, exercises the fallback; current CI default) 44 / 44
1.17.14 (serves the stable RPC, no fallback) 44 / 44

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 unrelated UNKNOWN not 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:

  • Code compiles correctly
  • Created/updated tests
  • Extended the documentation

@markjgardner
markjgardner requested review from a team as code owners September 17, 2026 20:50
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>
@markjgardner

Copy link
Copy Markdown
Contributor Author

@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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 Medium severity

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

codecov Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 4.25532% with 45 lines in your changes missing coverage. Please review.
✅ Project coverage is 47.29%. Comparing base (d1bf38a) to head (c112e07).
⚠️ Report is 128 commits behind head on main.

Files with missing lines Patch % Lines
src/implementation/Client/GRPCClient/pubsub.ts 7.69% 24 Missing ⚠️
src/implementation/Client/HTTPClient/pubsub.ts 0.00% 21 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (d1bf38a) and HEAD (c112e07). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (d1bf38a) HEAD (c112e07)
1 0
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     
Flag Coverage Δ
e2e 24.72% <4.25%> (?)
unit 40.97% <2.12%> (?)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@WhitWaldo

Copy link
Copy Markdown
Contributor

@markjgardner Could you please address Copilot's feedback?

WhitWaldo and others added 2 commits September 29, 2026 17:27
…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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The HTTP capability probe can silently publish batches to the wrong destination on older runtimes.

1 open issue
2 resolved since last review

Review effort: Balanced

WhitWaldo and others added 2 commits October 18, 2026 19:41
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>
@WhitWaldo

Copy link
Copy Markdown
Contributor

@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>
@markjgardner

Copy link
Copy Markdown
Contributor Author

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.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

HTTP serialization failures bypass the existing handler and reject instead of returning failed messages.

1 open issue
1 resolved since last review

Review effort: Balanced

@WhitWaldo
WhitWaldo merged commit 28a6da3 into dapr:main Oct 19, 2026
10 of 12 checks passed
@WhitWaldo

Copy link
Copy Markdown
Contributor

@markjgardner Thank you very much for taking the time to put this PR together and contribute to this project. I certainly appreciate it!

@marcduiker

Copy link
Copy Markdown
Contributor

@holopin-bot @markjgardner sdk-badge Thank you! Here's a digital badge as a small token of appreciation.

@holopin-bot

holopin-bot Bot commented Oct 20, 2026

Copy link
Copy Markdown

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.
Or if you're new to Holopin, you can simply sign up with GitHub, which will do the trick!

@nelson-parente nelson-parente added this to the v1.19 milestone Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(pubsub): Promote BulkPublishEventAlpha1 to stable BulkPublishEvent

5 participants