Skip to content

Make BundledHTTPRequest's init checks hold, and add Equatable - #38

Merged
germ-mark merged 1 commit into
mainfrom
llm/bundled-request-invariants
Jul 29, 2026
Merged

Make BundledHTTPRequest's init checks hold, and add Equatable#38
germ-mark merged 1 commit into
mainfrom
llm/bundled-request-invariants

Conversation

@germ-mark

Copy link
Copy Markdown
Contributor

Split out of #37, which had grown core-library changes inside a testing-mocks PR. This is independent of #32 and targets main.

init rejects a .get with a body and a url with no scheme, but both stored properties were publicly settable, so a caller could assign a body onto a .get, or flip the method of one that had a body, and reach a state init rejects. Validation was advisory rather than structural — which is also why the guard in URLSession.data(for:) is load-bearing rather than redundant.

body is now let and request is private(set). Header fields cannot invalidate either check, so settingHeader(_:for:) keeps them settable through a narrow API.

Two bugs found while writing the tests

  • missingScheme was unreachable in debug. init called assert(false) before throwing it, so that path trapped rather than surfacing the error — and could not be tested at all. My first attempt at a test segfaulted the suite.

  • urn: and mailto: urls produced requests that could never be sent. HTTPRequest drops everything after the scheme for those and reports url == nil. Anything comparing through request.url then called every url of that shape equal to every other:

    BundledHTTPRequest(url: URL("urn:isbn:1")!) == BundledHTTPRequest(url: URL("urn:isbn:2")!)
    observed: true
    

    init now rejects them with the new HTTPRequestError.unrepresentableURL. Equatable is synthesized — HTTPRequest is already Hashable — so it compares scheme, authority and path structurally rather than depending on that rejection staying in place.

Source-breaking

oauth4swift assigns header fields in place at three sites (DPoP/Signing.swift, and twice in Session/OAuthSession+AuthRequest.swift) and needs the companion migration to settingHeader(_:for:). I verified the break rather than assuming it: unfixed oauth4swift main against this fails with "'request' setter is inaccessible", and builds clean with the migration. A multiline-aware sweep of oauth4swift, AtprotoOAuth, AtprotoClient, GermShared, CoreAppLogic and ATResolve found no other mutator.

The companion commit is ready on oauth4swift's llm/adopt-germconvenience-api, waiting on this release. Note minor doesn't actually gate consumers on 0.x, since SwiftPM's from: is upToNextMajor — the lockstep release is what protects them.

Test notes

38 tests, 10 suites, clean build, no warnings. settingHeader is covered including the two behaviours it inherits from HTTPFields: repeated fields collapse to one comma-joined value, and a pseudo-header name traps.

Once this lands, #32's extension BundledHTTPRequest: Equatable in GermConvenienceMocks should be deleted — it would otherwise be a duplicate conformance. There's a comment marking it.

🤖 Generated with Claude Code

init() rejects a .get with a body and a url with no scheme, but both stored
properties were publicly settable, so a caller could assign a body onto a .get
or flip the method of one that had a body and reach a state init() rejects.
Validation was advisory rather than structural.

body is now let and request is private(set). Header fields cannot invalidate
either check, so settingHeader(_:for:) keeps them settable through a narrow API.

Two bugs found while writing the tests:

init() called assert(false) before throwing missingScheme, so in a debug build
that path trapped rather than surfacing the error, and could not be tested.

HTTPRequest cannot represent a urn: or mailto: url - it drops everything after
the scheme and reports url == nil. Such a request could never be sent, and any
comparison through request.url called it equal to every other url of that shape.
init() now rejects them with unrepresentableURL, and Equatable is synthesized so
it compares scheme, authority and path structurally.

Source-breaking: oauth4swift assigns headers in place at three sites and needs
the companion change to settingHeader(_:for:). No other consumer mutates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 83f028d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@germ-network/germ-convenience Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@germ-mark
germ-mark merged commit aa0dda2 into main Jul 29, 2026
10 checks passed
@germ-mark
germ-mark deleted the llm/bundled-request-invariants branch July 29, 2026 06:33
@github-actions github-actions Bot mentioned this pull request Jul 29, 2026
germ-mark added a commit that referenced this pull request Jul 29, 2026
The copy here would not have failed the build - it compiles with only
"conformance was already stated in the type's module", and the owner's witness
silently wins - so the mocks' == would have been shadowed rather than rejected.

Also covers the .head rejection this branch added, which had no test, and the
matching case where a bodiless method carries no body.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

1 participant