Skip to content

Fix refused-publish errors, add annotation query params and restore FallbackHosts getter - #1363

Open
sacOO7 wants to merge 5 commits into
uts/realtime-uts-tests-and-infrafrom
fix/new-api-review-follow-ups
Open

sacOO7 wants to merge 5 commits into
uts/realtime-uts-tests-and-infrafrom
fix/new-api-review-follow-ups

Conversation

@sacOO7

@sacOO7 sacOO7 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR fixes four issues found while reviewing the public API added by the protocol v6 and Endpoint PRs (#1346, #1347, #1348, #1349, #1359) against the features spec and ably-java:

# Fix Kind User-visible?
1 A refused realtime PublishAsync now reports the real error (40000) instead of a generic 50000 Behaviour fix Yes
2 AnnotationsRequestParams can send any query parameter, not just limit API addition Yes
3 ClientOptions.FallbackHosts can be read again API addition (restores read access) Yes
4 Realtime channels log through their client's logger Internal fix No (test infrastructure only)

Each fix comes with native tests. Where a UTS test had been gated or adapted around the problem, it now runs as the spec writes it.

Stacked PR. The base is uts/realtime-uts-tests-and-infra (#1362), because the UTS tests touched here only exist on that branch. Once #1362 merges, retarget this PR to integration/v2.


Changes

1. A refused realtime publish surfaces its own error

Problem. When PublishAsync was refused because of the channel or connection state, the task faulted with a generic 50000 "Unexpected error". The real refusal (40000, status 400) was only reachable through InnerException. UpdateMessageAsync, DeleteMessageAsync, AppendMessageAsync and Annotations.PublishAsync already surface the original error, so the same channel reported different errors depending on the method.

A refusal happens when the channel is SUSPENDED or FAILED, or the connection is neither CONNECTED nor allowed to queue (RTL6c4).

Fix. The realtime task wrappers (TaskWrapper, TaskWrapper<T>, PublishTaskWrapper) now pass an AblyException through unchanged. Any other exception is still wrapped as 50000, as before. The "can't queue" refusal message now names the connection state instead of the channel state.

Also affected. The same wrappers back AttachAsync, DetachAsync, SetOptionsAsync, the presence methods and PingAsync. Those calls now fault with the underlying AblyException too. For example, with QueueMessages = false and the connection not CONNECTED, AttachAsync faults with "Not queuing messages …" (code 50000, status 400) instead of a wrapping "Unexpected error".

Docs. The XML docs on the three IRealtimeChannel.PublishAsync overloads and on UpdateMessageAsync now describe this correctly. CHANGELOG and UPDATING list it as a behaviour change.

2. AnnotationsRequestParams accepts any query parameter

Problem. Annotations.GetAsync could only send limit. The spec (RSAN3a/b; the IDL types the parameter as params?: Dict<String, Stringifiable>) and ably-java (Param[]) allow any parameter.

Fix.

  • New property: AnnotationsRequestParams.ExtraParameters (Dictionary<string, string>), the same shape as PaginatedRequestParams.ExtraParameters. Every entry goes into the querystring.
  • limit is sent exactly once, in this order of precedence:
    1. the typed Limit, if set;
    2. otherwise an ExtraParameters["limit"] entry;
    3. otherwise the default of 100.
  • Realtime gets this too, because realtime Annotations.GetAsync delegates to REST.

Why ExtraParameters and not Params. In the IDL, params names the whole argument of get(messageSerial, params?: Dict<String, Stringifiable>), not a field inside it. ably-java likewise takes the whole argument as get(String messageSerial, Param[] params). In .NET that argument is the AnnotationsRequestParams query object; it is named query because params is a C# keyword, as in the existing HistoryAsync and GetMessageVersionsAsync methods. The closest precedent is getMessageVersions(serial, params?: Dict<String, Stringifiable>). Its IDL argument has exactly the same shape, and .NET already maps it to GetMessageVersionsAsync(serial, PaginatedRequestParams query): typed members for the common parameters, plus PaginatedRequestParams.ExtraParameters for any other entry. AnnotationsRequestParams follows the same pattern: the typed Limit, plus ExtraParameters. (Channel and presence history are different: their IDL lists only typed fields, and ExtraParameters is a .NET extension there.)

Behaviour change. A Limit outside 0–1000 now throws an AblyException (code 40003, status 400), like the other argument checks in Annotations. It previously threw an ArgumentException. Annotations are a new 2.0 API.

3. ClientOptions.FallbackHosts is readable again

Problem.

Fix. FallbackHosts is now a normal get/set property, like every other ClientOptions member.

  • What the getter returns: what you set, or null when unset. It never returns the defaults derived from Endpoint, which matches Java and JS.
  • Effective list: the internal GetFallbackHosts() still resolves the list actually used for fallback (REC2), unchanged.
  • Cloning: Clone() now copies the property directly.

Migration note (in UPDATING). The old public GetFallbackHosts() returned the effective list, including defaults. The new getter returns only what was set.

4. Realtime channels log through their client's logger

Problem. Channels created with Channels.Get(name) used the process-wide default logger instead of their client's logger.

  • Why users never see a difference: ClientOptions.Logger is internal, and every client configures that same shared default logger through LogHandler and LogLevel.
  • Why it still matters: test clients that use their own internal logger never saw channel warnings, such as the RTAN4e "AnnotationSubscribe mode not granted" warning. That's why the RTAN4e UTS test had been gated.

Fix.

  • RealtimeChannel now uses options?.Logger ?? realtimeClient.Logger, so a logger passed in on purpose still wins.
  • The public ChannelOptions constructors no longer capture the static logger.
  • Presence, Annotations and the attach/detach awaiters follow the channel's logger.

User impact. None. This isn't in the CHANGELOG.


Tests

Native tests (new)

File Covers
Realtime/PublishResultSpecs.cs Refusal in SUSPENDED and FAILED, and when the connection can't queue, returns the original 40000 error. Guard: an unexpected exception is still wrapped as 50000.
Http/ChannelAnnotationsSpecs.cs Extra params reach the querystring; limit precedence; null ExtraParameters; an out-of-range Limit gives 40003; the 0 and 1000 bounds.
Realtime/AnnotationsSpecs.cs Extra params over realtime.
ClientOptionsTests.cs FallbackHosts: unset reads null; set, empty and reset values read back; cloning.
Realtime/ChannelsSpecs.cs A channel, with or without each public ChannelOptions constructor, and its Presence use the client's logger; an explicitly passed logger wins; the RTAN4e warning reaches a client-specific sink.

With each fix reverted, its new tests fail. The exceptions are the two guard tests, which are meant to pass either way.

UTS tests

These tests now run as the spec writes them, and their deviations.md entries are removed:

Test Before Now
RealtimeTests.Unit.ChannelAnnotationsTests.RTAN4e_SubscribeWarnsNoMode Gated [DeviationFact] Runs, and passes
RestTests.Unit.AnnotationsTests.NoId_Rsan3bGetPassesParamsAsQuerystring Adapted to { Limit = 50 } Passes the spec's { "limit": "50" }
RestTests.Unit.OptionsTypesTests.TO3_ClientOptionsCustomHosts Read the internal GetFallbackHosts() Reads the public FallbackHosts

The uts-to-csharp skill's mapping notes (rest-mapping.md, realtime-mapping.md) are updated to match.


Validation

An independent reviewer checked this PR before it was opened. It confirmed that each fix addresses the root cause, the new tests fail without the fixes, the UTS edits are faithful to the spec, and the docs are accurate. Results (net6.0, Release):

Check Result
./build.sh --target=Build.NetStandard (StyleCop as errors) 0 warnings, 0 errors
Native unit suite (Test.NetStandard.Unit) 1479 passed, 19 skipped, 0 failed
UTS unit tiers (REST + realtime) 735 passed, 259 skipped, 0 failed
UTS unit tiers with RUN_DEVIATIONS=1 Every failure is a gated [DeviationFact] test; the un-gated tests pass
UTS integration (REST + realtime, sandbox) 134 passed, 0 failed (one unrelated proxy test, RTN15j_FatalErrorEstablishedConn, failed once on a race and passed on re-run)

Not run locally: the .NET Framework legs (they need Windows or Mono) and net7.0–net9.0. CI covers these.


Size

23 files, +417 / −112:

Area Lines
Product code (src/Ably.PubSub.Shared, Side) +114 / −51, about a third of it XML docs
Native tests +281 / −18
UTS tests and deviations.md +3 / −32
CHANGELOG / UPDATING +13 / −4
Skill mapping notes +6 / −7

Commits

  1. Log realtime channels through the client's logger: fix 4.
  2. Surface the original error when a realtime publish is refused: fix 1.
  3. Send arbitrary query params with Annotations.GetAsync: fix 2.
  4. Make ClientOptions.FallbackHosts readable: fix 3.
  5. Document the unwrapped realtime task errors and annotation params: follow-up doc fixes from the review.

Reviewer notes

  • Behaviour changes to call out:
    • Fix 1: error codes and messages for refused or unqueueable realtime calls.
    • Fix 2: ArgumentException → AblyException 40003.
    • Both are in CHANGELOG and UPDATING.
  • Out of scope:
    • The message encoders still fall back to the static logger. That's unchanged, not a regression.
    • Limit validation is still inconsistent across older APIs (REST presence history throws ArgumentException).

Summary by CodeRabbit

  • New Features

    • Added support for arbitrary annotation query parameters, including limit handling and validation.
    • ClientOptions.FallbackHosts can now be read, with clear behavior for configured, empty, and unset values.
  • Bug Fixes

    • Preserved detailed refusal errors for publish and update operations instead of replacing them with generic errors.
    • Improved connection-state messages when publishing is refused.
    • Channel logging now correctly inherits the client logger when no logger is specified.
  • Documentation

    • Updated migration guidance and API documentation for fallback hosts, annotations, and realtime error behavior.

RealtimeChannel took its logger from ChannelOptions.Logger only, which is
null for Channels.Get(name) and is set to the static default logger by
every public ChannelOptions constructor. Channels, their Presence and
Annotations therefore never used a client's own logger, so the RTAN4e
"AnnotationSubscribe not granted" warning missed a per-client log sink.

ChannelOptions.Logger now stays null unless a logger is explicitly
supplied (internal constructor), and the channel falls back to the
client's logger. An explicitly supplied logger still wins. The message
encoders keep falling back to the default logger when it is null.

Un-gate the UTS test RTAN4e_SubscribeWarnsNoMode and drop its
deviations.md entry; add native ChannelsSpecs tests for both Get forms.
PublishAsync caught the AblyException that EnsureCanPublish throws for a
channel in SUSPENDED/FAILED or a connection that can't queue (RTL6c4) and
wrapped it as a generic 50000 "Unexpected error", keeping the 40000 only
as the InnerException. The edit methods and annotation publish already
surfaced the 40000 unwrapped.

The task wrappers now pass an AblyException through as is and wrap only
non-Ably exceptions as 50000. The connection refusal message now names
the connection state instead of the channel state.

Correct the IRealtimeChannel PublishAsync and UpdateMessageAsync XML docs,
which said every failure is a failed Result: a refusal faults the task.
Update the uts-to-csharp realtime mapping, CHANGELOG and UPDATING, and add
native PublishResultSpecs tests for the refusal codes and for the
unchanged wrapping of unexpected exceptions.
RSAN3a/RSAN3b (IDL params?: Dict<String, Stringifiable>, RTAN3a for
realtime) let annotations.get send any query parameter, and ably-java
takes Param[]. AnnotationsRequestParams exposed only Limit.

Add AnnotationsRequestParams.ExtraParameters, mirroring
PaginatedRequestParams.ExtraParameters. Every entry goes to the
querystring; the typed Limit, when set, wins over a "limit" entry, and
the default limit of 100 is still sent when neither is set. An
out-of-range Limit now throws AblyException 40003 (as the other
annotation argument checks do) instead of ArgumentException.

The RSAN3b UTS test now passes the spec's params { "limit": "50" }
straight, so its adaptation and deviations.md entry are removed and
the mapping references updated.
The IDL lists fallbackHosts as a ClientOptions attribute (TO3k6,
"String[] default nil"), and ably-java and ably-js expose it as a
readable field. FallbackHosts was set-only, and since the Endpoint
change made GetFallbackHosts() internal there was no public way to
read it.

FallbackHosts is now a get/set auto-property like every other
ClientOptions member. It returns the hosts that were set, or null when
none were; it does not return the default fallback hosts derived from
Endpoint, which the internal GetFallbackHosts() still resolves. Clone()
copies it through the property, so the write-only special case in the
clone rot-guard is gone.

The TO3k6 UTS test now reads options.FallbackHosts as the spec does,
so its adaptation and deviations.md entry are removed and the mapping
references updated.
UPDATING now says the unwrapped AblyException also applies to attach,
SetOptions and presence calls (for example the "Not queuing messages"
error when QueueMessages is false). The CHANGELOG moves the annotation
Limit validation change under Behaviour changes and drops the channel
logger bullet, which has no user-visible effect. The realtime
Annotations.GetAsync query docs and the rest-mapping annotations row
now match the REST behaviour.
@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 88f8f984-4f7e-4249-8804-5910000283cd
📥 Commits

Reviewing files that changed from the base of the PR and between 8bf0ffd and f4f7931.

📒 Files selected for processing: 23
  • .claude/skills/uts-to-csharp/references/realtime-mapping.md
  • .claude/skills/uts-to-csharp/references/rest-mapping.md
  • CHANGELOG.md
  • UPDATING.md
  • src/Ably.PubSub.Shared/ClientOptions.cs
  • src/Ably.PubSub.Shared/Http/AnnotationsRequestParams.cs
  • src/Ably.PubSub.Shared/Http/ChannelOptions.cs
  • src/Ably.PubSub.Shared/Http/RestAnnotations.cs
  • src/Ably.PubSub.Shared/Realtime/IRealtimeChannel.cs
  • src/Ably.PubSub.Shared/Realtime/RealtimeAnnotations.cs
  • src/Ably.PubSub.Shared/Realtime/RealtimeChannel.cs
  • src/Ably.PubSub.Shared/Transport/TaskWrapper.cs
  • src/Ably.PubSub.Side/Side.cs
  • src/Ably.PubSub.Tests.DotNET/PubSub/ClientOptionsCloneSpecs.cs
  • src/Ably.PubSub.Tests.Shared/ClientOptionsTests.cs
  • src/Ably.PubSub.Tests.Shared/Http/ChannelAnnotationsSpecs.cs
  • src/Ably.PubSub.Tests.Shared/Realtime/AnnotationsSpecs.cs
  • src/Ably.PubSub.Tests.Shared/Realtime/ChannelsSpecs.cs
  • src/Ably.PubSub.Tests.Shared/Realtime/PublishResultSpecs.cs
  • src/Ably.PubSub.Tests.Shared/Uts/RealtimeTests/Unit/ChannelAnnotationsTests.cs
  • src/Ably.PubSub.Tests.Shared/Uts/RestTests/Unit/AnnotationsTests.cs
  • src/Ably.PubSub.Tests.Shared/Uts/RestTests/Unit/OptionsTypesTests.cs
  • src/Ably.PubSub.Tests.Shared/Uts/deviations.md
💤 Files with no reviewable changes: 1
  • src/Ably.PubSub.Tests.Shared/Uts/deviations.md

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

The changes expose configured fallback hosts, support extra annotation query parameters, preserve realtime AblyException details, and apply client logger fallback to realtime channels. Tests and documentation were updated for these behaviors.

Changes

Fallback host configuration

Layer / File(s) Summary
Expose and carry configured fallback hosts
src/Ably.PubSub.Shared/ClientOptions.cs, src/Ably.PubSub.Side/Side.cs, src/Ably.PubSub.Tests.DotNET/PubSub/ClientOptionsCloneSpecs.cs, src/Ably.PubSub.Tests.Shared/ClientOptionsTests.cs, src/Ably.PubSub.Tests.Shared/Uts/RestTests/Unit/OptionsTypesTests.cs, .claude/skills/uts-to-csharp/references/rest-mapping.md, CHANGELOG.md, UPDATING.md, src/Ably.PubSub.Tests.Shared/Uts/deviations.md
FallbackHosts is now readable and writable. It returns the explicitly configured value, or null when unset; GetFallbackHosts() continues to derive defaults from Endpoint. Clone behavior, tests, and documentation reflect these rules.

Annotation query parameters

Layer / File(s) Summary
Build and send annotation query parameters
src/Ably.PubSub.Shared/Http/AnnotationsRequestParams.cs, src/Ably.PubSub.Shared/Http/RestAnnotations.cs, src/Ably.PubSub.Shared/Realtime/RealtimeAnnotations.cs, src/Ably.PubSub.Tests.Shared/Http/ChannelAnnotationsSpecs.cs, src/Ably.PubSub.Tests.Shared/Realtime/AnnotationsSpecs.cs, src/Ably.PubSub.Tests.Shared/Uts/RestTests/Unit/AnnotationsTests.cs, src/Ably.PubSub.Tests.Shared/Uts/deviations.md, .claude/skills/uts-to-csharp/references/*mapping.md, CHANGELOG.md, UPDATING.md
AnnotationsRequestParams.ExtraParameters carries arbitrary query parameters. An explicit Limit takes precedence over an extra limit; otherwise the extra value or default is used. Limits outside 0–1000 throw AblyException code 40003 with HTTP 400.

Realtime exception handling

Layer / File(s) Summary
Preserve realtime refusal errors
src/Ably.PubSub.Shared/Transport/TaskWrapper.cs, src/Ably.PubSub.Shared/Realtime/RealtimeChannel.cs, src/Ably.PubSub.Shared/Realtime/IRealtimeChannel.cs, src/Ably.PubSub.Tests.Shared/Realtime/PublishResultSpecs.cs, .claude/skills/uts-to-csharp/references/realtime-mapping.md, CHANGELOG.md, UPDATING.md
Task wrappers preserve existing AblyException instances and wrap other exceptions. Publish refusals expose their original error details; tests verify refusal errors and unexpected exception wrapping.

Channel logger fallback

Layer / File(s) Summary
Apply client logger to channels
src/Ably.PubSub.Shared/Http/ChannelOptions.cs, src/Ably.PubSub.Shared/Realtime/RealtimeChannel.cs, src/Ably.PubSub.Tests.Shared/Realtime/ChannelsSpecs.cs, src/Ably.PubSub.Tests.Shared/Uts/RealtimeTests/Unit/ChannelAnnotationsTests.cs, src/Ably.PubSub.Tests.Shared/Uts/deviations.md
When no channel logger is supplied, the realtime channel uses the client logger. An explicit channel logger overrides it. Tests cover logger inheritance and annotation subscription warnings.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix

Merge Risk: ⚪ Minimal · up to f4f79

The annotation query behavior matches the documented API contract. No issue identified here needs to be resolved before merging.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 34.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 61 functions across 18 files. (4 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely summarizes the main changes: refused-publish error handling, annotation query parameters, and restoration of the FallbackHosts getter.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 34.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 61 functions across 18 files. (4 skipped: 4 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

I’m a rabbit, hopping past the host list,
I find extra query keys tucked in the request.
The limit stays clear, from zero to one thousand,
Refusal errors keep the details they carried.
The client’s logger joins each quiet channel,
And I thump my paws at tests that pass.

Comment @coderabbitai help to get the list of available commands.

@ttypic ttypic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM

This branch was successfully deployed

1 active deployment
staging/pull/1363/features — f4f7931a Deployed Oct 21, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants