Skip to content

fix: percent-encode keys in local reference pointers - #62

Merged
VisualBean merged 2 commits into
ByteBardOrg:vnextfrom
bielu:fix/percent-encoded-reference-keys
Oct 24, 2026
Merged

VisualBean merged 2 commits into
ByteBardOrg:vnextfrom
bielu:fix/percent-encoded-reference-keys

Conversation

@bielu

@bielu bielu commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #61.

Since #60, the workspace registered keys escaped with only ~0 / ~1, while names recovered from references go through JsonPointer.FromUriFragment, which percent-decodes (RFC 6901 §6). Keys containing % or characters that are percent-encoded in a URI fragment did not round-trip:

  • A 2.x server named a%20b was written back to 2.x as a b, with no error. Upgrading to 3.x produced $ref: '#/servers/a%20b', which points at a server named a b.
  • A 3.x $ref: '#/servers/a%20b' missed the workspace entry for a b and was resolved from the raw document into a separate copy, so changes to document.Servers["a b"] didn't reach it.

While investigating, I found a related bug that also affects valid documents. The registry was keyed by Uri, and Uri equality ignores the fragment of absolute URIs. So https://example.com/s.json#/A and https://example.com/s.json#/B shared an entry, and with ResolveAllReferences the second reference silently resolved to the first schema.

Changes

  • JsonPointer.EscapeUriFragmentToken(token) and JsonPointer.ToUriFragment() are added as the reverse of FromUriFragment. They escape each token with ~0 / ~1, then apply Uri.EscapeDataString.
  • AsyncApiWorkspace keys its registries by string. Each location's fragment is normalized into that one percent-encoded form, so encoded and unencoded references (#/servers/a b, #/servers/a%20b) find the same entry, and fragments of absolute URIs are compared.
  • Workspace registrations and the references the 2.x reader builds use EscapeUriFragmentToken instead of EscapeToken.
  • The 2.x reader matches operation channels and message keys by the decoded last pointer token instead of splitting the raw reference string.

Only keys with characters that get percent-encoded are affected. Keys that match the spec's key patterns produce the same references as before. One side effect: the resource part of an external location is now compared as written, so HTTP://Example.com/s.json and http://example.com/s.json are no longer treated as the same resource.

Tests

  • PercentEncodedReferenceKeys_Should covers a key with a space (prod eu) next to a key that is literally prod%20eu, through 2.x→2.x, 3.x→3.x, 2.x→3.x and 3.x→2.x, in the style of EscapedReferenceKeys_Should. It also checks that encoded and unencoded 3.x server references resolve to the root server rather than a copy.
  • AsyncApiReference_Should.AbsoluteExternalReferences_ToTheSameResource_ResolveTheirOwnFragments covers the absolute URI case.
  • JsonPointer_Should has unit tests for the new helpers.

8 of the new tests fail without the fix. The full suite (503 tests) passes on net8.0, net9.0 and net10.0.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Fixed resolution of percent-encoded reference keys across AsyncAPI 2.x and 3.x documents, including references to channels, messages, servers, and components.
    • Fixed handling of multiple references to the same external resource when each points to a different fragment.
    • Improved preservation of references when converting between AsyncAPI versions and when reading and writing documents.

@bielu
bielu requested a review from VisualBean as a code owner October 24, 2026 22:15
@coderabbitai

coderabbitai Bot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 3 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: f9ff896b-dc45-40c1-b74b-1b27854363f8
Commits

Reviewing files that changed from the base of the PR and between 5d438c6 and e0a976b.

Files selected for processing: 10
  • src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs
  • src/ByteBard.AsyncAPI.Readers/V2/AsyncApiDocumentDeserializer.cs
  • src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationDeserializer.cs
  • src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSecurityRequirementDeserializer.cs
  • src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs
  • src/ByteBard.AsyncAPI/JsonPointer.cs
  • src/ByteBard.AsyncAPI/Models/References/AsyncApiChannelReference.cs
  • test/ByteBard.AsyncAPI.Tests/JsonPointer_Should.cs
  • test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs
  • test/ByteBard.AsyncAPI.Tests/Models/PercentEncodedReferenceKeys_Should.cs

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ba737e80-261c-4fea-a0c8-4f87359e2c99
Commits

Reviewing files that changed from the base of the PR and between 4fcc2ad and 5d438c6.

Files selected for processing: 9
  • src/ByteBard.AsyncAPI.Readers/V2/AsyncApiChannelDeserializer.cs
  • src/ByteBard.AsyncAPI.Readers/V2/AsyncApiDocumentDeserializer.cs
  • src/ByteBard.AsyncAPI.Readers/V2/AsyncApiOperationDeserializer.cs
  • src/ByteBard.AsyncAPI.Readers/V2/AsyncApiSecurityRequirementDeserializer.cs
  • src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs
  • src/ByteBard.AsyncAPI/JsonPointer.cs
  • test/ByteBard.AsyncAPI.Tests/JsonPointer_Should.cs
  • test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs
  • test/ByteBard.AsyncAPI.Tests/Models/PercentEncodedReferenceKeys_Should.cs

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 change adds URI-fragment escaping and serialization for JSON Pointer tokens. Workspace registries normalize valid URI-fragment pointers as string keys. V2 readers use the updated escaping and token decoding when constructing and matching references.

Changes: URI-fragment reference keys

Layer / File(s) Summary
URI-fragment pointer helpers
src/ByteBard.AsyncAPI/JsonPointer.cs, test/ByteBard.AsyncAPI.Tests/JsonPointer_Should.cs
Adds token escaping and URI-fragment serialization helpers. Tests cover percent encoding, pointer escaping, decoding, and round trips.
Workspace key normalization
src/ByteBard.AsyncAPI/AsyncApiWorkspace.cs, test/ByteBard.AsyncAPI.Tests/Models/AsyncApiReference_Should.cs
Changes workspace registries to ordinal string keys. Valid URI-fragment pointers are normalized before registration, containment checks, and reference resolution. Adds a test for distinct fragments on absolute references to the same resource.
V2 reference construction and resolution
src/ByteBard.AsyncAPI.Readers/V2/*Deserializer.cs, test/ByteBard.AsyncAPI.Tests/Models/PercentEncodedReferenceKeys_Should.cs
V2 readers escape reference keys for URI fragments and decode final tokens when matching channels and messages. Tests cover encoded keys, reference resolution, round trips, and V2/V3 conversions.

Priority: Normal

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

Change: Bug fix · Severity of issue fixed: Low

Possibly related PRs

  • ByteBardOrg/AsyncAPI.NET#60: Adds JSON Pointer escaping for local keys in the workspace and V2 reader paths, which this change extends with URI-fragment percent encoding.
  • ByteBardOrg/AsyncAPI.NET#57: Adds local V2 channel server references; this change updates matching and workspace registration for percent-encoded server keys.
  • ByteBardOrg/AsyncAPI.NET#56: Adds the local JSON Pointer parser and URI-fragment parsing that this change extends with escaping and serialization.
Merge Risk: Minimal · up to 5d438

No actionable issue is established for this change. It is mergeable after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 22.58% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 31 functions across 9 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Title check Passed The title "fix: percent-encode keys in local reference pointers" matches the main change. The PR adds URI-fragment escaping helpers, switches workspace registrations and 2.x references to escaped toke…
Description check Passed The description follows the template. It has an About section with a detailed explanation of the problem and the fix, a Changes section, and a Tests section. It references the linked issue (Fixes #61)…
Linked Issues check Passed Issue #61 is the only active direct link. Its coding requirements are met at the reviewed head, based on the whole-PR diff and the head file read.

1. Add the reverse of FromUriFragment (ToUriFragment…
Out of Scope Changes check Passed No demonstrated unrelated change was found. Each changed area connects to #61 or to the mechanism #61 requires.

- JsonPointer helpers (EscapeUriFragmentToken, ToUriFragment): required by #61.
- Async…
Full details: Title check

Explanation

The title "fix: percent-encode keys in local reference pointers" matches the main change. The PR adds URI-fragment escaping helpers, switches workspace registrations and 2.x references to escaped tokens, and fixes percent-encoded reference keys that failed to round-trip. The title is short, specific, and follows a conventional-commit style. It omits the registry keying change and the absolute-URI fix, which are secondary.

Full details: Description check

Explanation

The description follows the template. It has an About section with a detailed explanation of the problem and the fix, a Changes section, and a Tests section. It references the linked issue (Fixes #61), which satisfies the Related Issues part of the template. The description does not use the template's Changelog section with Add/Remove entries, so that part of the template is missing.

Full details: Linked Issues check

Explanation

Issue #61 is the only active direct link. Its coding requirements are met at the reviewed head, based on the whole-PR diff and the head file read.

  1. Add the reverse of FromUriFragment (ToUriFragment): met. src/ByteBard.AsyncAPI/JsonPointer.cs at head adds EscapeUriFragmentToken (EscapeToken, then Uri.EscapeDataString) and ToUriFragment (per-token "/" + EscapeUriFragmentToken).

  2. Normalize the fragment of every location into one form in the workspace: met. AsyncApiWorkspace.cs replaces ToLocationUrl with static NormalizeLocation. It rewrites the fragment through TryFromUriFragment and ToUriFragment. RegisterComponent, Contains and ResolveReference all call it. The registries are now string-keyed, so encoded and unencoded references (#/servers/a b and #/servers/a%20b) map to one entry.

  3. Build workspace registrations with the same helper instead of EscapeToken: met. The whole-PR diff shows every component, channel, server and message registration location using EscapeUriFragmentToken.

  4. Build the 2.x reader's references with the same helper: met per the whole-PR change summary. AsyncApiChannelDeserializer (GetServerReferenceKey), AsyncApiDocumentDeserializer (reassigned channel references, message tokens), AsyncApiOperationDeserializer (LoadMessages) and AsyncApiSecurityRequirementDeserializer use EscapeUriFragmentToken. The reader source diffs were not read directly. The 2.x reader matches channels and messages by the decoded last pointer token (GetLastReferenceToken).

  5. Tests for a key containing % and a key containing a space, through 2.x to 2.x, 3.x to 3.x, 2.x to 3.x and 3.x to 2.x: met per the change summary. PercentEncodedReferenceKeys_Should contains V2_RoundTrips, V3_RoundTrips, V3_DowngradesToV2 and V2_UpgradesToV3. Its key pair is 'prod eu' and 'prod%20eu'. The test bodies were not read. The author reports 8 new tests fail without the fix and the 503-test suite passes. These results were not reproduced.

Valid-document behavior is unchanged for keys that match the spec key pattern. Such keys contain only unreserved characters, so EscapeUriFragmentToken returns the same text as EscapeToken.

Evidence limits: the diff for JsonPointer.cs was unavailable in the checkout, so the helper code was read from the head file. The 2.x reader and test file contents were assessed from the supplied change summary.

Full details: Out of Scope Changes check

Explanation

No demonstrated unrelated change was found. Each changed area connects to #61 or to the mechanism #61 requires.

  • JsonPointer helpers (EscapeUriFragmentToken, ToUriFragment): required by #61.
  • AsyncApiWorkspace string-keyed registries and NormalizeLocation: #61 asks that every location fragment be normalized through ToLocationUrl. Storing the normalized fragment needs string keys, because Uri equality ignores the fragment of absolute URIs. The absolute-URI fix is therefore a consequence of the same change, and the PR states it as a related bug.
  • 2.x reader changes (channel and message key matching by decoded last token, escaped references): needed so that encoded keys round-trip through 2.x. #61 asks for 2.x references to use the same helper.
  • Tests (PercentEncodedReferenceKeys_Should, the absolute-URI test, JsonPointer_Should): support the requirements in #61 and the absolute-URI fix above.

One behavior change goes beyond #61 and is disclosed in the PR description. External resource locations are now compared as written. Scheme and host case no longer normalize, so HTTP://Example.com/s.json and http://example.com/s.json are distinct. This follows from the string-key design and is not a requested outcome. Reviewers should decide whether this change is acceptable.

Finishing Touches
🧪 Generate unit tests (beta)
  • 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

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

@VisualBean

Copy link
Copy Markdown
Contributor
  1. Please rebase on vnext (fix!: resolve local references outside the components, in both versions #63 landed). The conflicts in JsonPointer.cs and AsyncApiWorkspace.cs are just "keep both" (keep fix!: resolve local references outside the components, in both versions #63's null check on channel references in RegisterComponents, with your escaping).

  2. fix!: resolve local references outside the components, in both versions #63 added AsyncApiChannelReference.SerializeV2, which writes 2.x channel references by address with JsonPointer.EscapeToken. It needs EscapeUriFragmentToken too: a channel with address a%20b currently round-trips to $ref: '#/channels/a%20b', which no longer resolves. Worth a test case in PercentEncodedReferenceKeys_Should.

bielu and others added 2 commits October 25, 2026 00:11
The workspace registered keys escaped with only "~0" / "~1", while names recovered
from references were percent-decoded (RFC 6901 section 6). Keys containing "%" or
characters that are percent-encoded in a URI fragment did not round-trip:

- A 2.x server named "a%20b" was written back as "a b", and upgraded to a 3.x
  "#/servers/a%20b" reference that points at a server named "a b".
- A 3.x "$ref: '#/servers/a%20b'" missed the workspace entry for "a b" and was
  resolved from the raw document into a separate copy of the server.

The workspace now normalizes every location's fragment into one percent-encoded
form, so encoded and unencoded references find the same entry, and registrations
and the 2.x reader's references are built with JsonPointer.EscapeUriFragmentToken.

Registry keys are now compared as strings instead of Uris. Uri equality ignores
the fragment of absolute URIs, so "https://x/s.json#/A" and "https://x/s.json#/B"
shared an entry and the second reference silently resolved to the first schema.

Fixes ByteBardOrg#61

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AsyncApiChannelReference.SerializeV2 writes a reference to a root channel with
the channel's address, escaped with EscapeToken only. A channel addressed
"a%20b" was written as "#/channels/a%20b", which decodes to "a b" and no longer
resolves. It now uses EscapeUriFragmentToken, as does the 2.x reader when it
points such a reference at the normalized channel key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@bielu
bielu force-pushed the fix/percent-encoded-reference-keys branch from 5d438c6 to e0a976b Compare October 24, 2026 23:12
@bielu

bielu commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, both done:

  1. Rebased on vnext. I kept both sides of the conflicts: fix!: resolve local references outside the components, in both versions #63's null check on channel references in RegisterComponents with the new escaping, and JsonPointer.Find next to the new helpers.
  2. AsyncApiChannelReference.SerializeV2 now uses EscapeUriFragmentToken (e0a976b). NormalizeChannelPointer in the 2.x reader, also from fix!: resolve local references outside the components, in both versions #63, had the same EscapeToken call, so it's switched too. I added PercentEncodedReferenceKeys_Should.V2_ChannelReference_RoundTripsByEncodedAddress for addresses a b and a%20b. Both cases fail without the change. The full suite passes on net8.0, net9.0 and net10.0.

@VisualBean
VisualBean merged commit 9149a76 into ByteBardOrg:vnext Oct 24, 2026
6 checks passed
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.

Percent-encoding is not handled consistently in local reference pointersc

2 participants