Skip to content

fix: trap when a mediaUploader is set without site credentials - #632

Closed
jkmassel wants to merge 5 commits into
refactor/media-upload-handler-objectfrom
fix/media-uploader-credentials-trap
Closed

jkmassel wants to merge 5 commits into
refactor/media-upload-handler-objectfrom
fix/media-uploader-credentials-trap

Conversation

@jkmassel

@jkmassel jkmassel commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Stacked on #631. Ninth of ten PRs splitting #621.

What?

Setting a mediaUploader without site credentials is a configuration error, and now fails fast instead of silently doing nothing.

Why?

Setting a mediaUploader means the host is taking over uploads. With no site credentials the server would previously just not start, silently dropping the uploader — and its media deletes still need the internal media client to reach the configured site, since every attachment lives there no matter who delivered it. Starting anyway would give a server whose every delete 500s.

How?

The behavior forks by intent:

  • A mediaProcessor with no credentials leaves the server down and uploads fall to the default WebView path — there is nothing to deliver through, so nothing to process.
  • A mediaUploader with no credentials traps: precondition on iOS, check on Android.

The iOS policy lives in MediaServerCredentials (added in #624) rather than EditorViewController, which is #if canImport(UIKit) and therefore absent from the macOS host — the one platform that can run Swift Testing's exit tests. Living outside the gate, the trap itself is testable, not just the predicate.

Testing Instructions

  • Two iOS exit tests run the trap in a child process; neutering the precondition fails both
  • Three Android tests through GutenbergView — the two uploader arms and the processor arm; neutering the check fails the two uploader tests
  • swift test — host suite green
  • Android :Gutenberg:testDebugUnitTest green
  • iOS Simulator xcodebuild
  • SwiftLint + Detekt clean

@jkmassel jkmassel added [Type] Bug An existing feature does not function as intended Android iOS labels Sep 5, 2026
@jkmassel jkmassel self-assigned this Sep 5, 2026
@wpmobilebot

wpmobilebot commented Sep 5, 2026 •

Copy link
Copy Markdown

XCFramework Build

This PR's XCFramework is available for testing. Add the following to your Package.swift:

.package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/632")

Built from 16a9557

@jkmassel
jkmassel force-pushed the fix/media-uploader-credentials-trap branch from 9ee12ea to eef9925 Compare September 8, 2026 15:15
@jkmassel
jkmassel force-pushed the fix/media-uploader-credentials-trap branch from eef9925 to 873c04d Compare September 8, 2026 16:12
@jkmassel
jkmassel force-pushed the fix/media-uploader-credentials-trap branch from 873c04d to 8506a8b Compare September 9, 2026 00:56
@jkmassel
jkmassel force-pushed the fix/media-uploader-credentials-trap branch from 8506a8b to 345809f Compare September 15, 2026 22:39
The protocol no longer uploads anything — the previous commit removed
`uploadFile`, leaving `handlesFile` and `processFile`. "UploadDelegate" now
describes the one thing it can't do, and next to `MediaUploader` the two
names read as variations on the same job rather than the two halves of a
deliberate split.

`MediaProcessor` says what is left: it transforms bytes, GutenbergKit
delivers them. Mechanical throughout — the property becomes
`mediaProcessor`, the server parameter `processor`, the file
`MediaHandlers.swift` (it holds both protocols now), and Android's demo
`DemoMediaProcessor`. Prose follows the types.

The `weak_delegate` suppression added when the property became strong goes
away with the name: the rule was arguably right that a strongly-held
"delegate" is a smell, and the answer was that this was never a delegate.

BREAKING CHANGE: `mediaUploadDelegate` is now `mediaProcessor`, and
`MediaUploadDelegate` is `MediaProcessor`. Conformances need no changes
beyond the name.
`MediaProcessor` and `MediaUploader` were both `AnyObject`-bound, and
`EditorViewController` holds both strongly. A conformer that holds the view
controller back therefore closes a retain cycle ARC cannot break: the editor
is never freed, so `deinit` never runs, so `uploadServer.stop()` — its only
caller — never runs either, and a bound loopback `NWListener` outlives the
editing session.

Nothing needed class-boundness. There is no `weak`, `===`, or
`ObjectIdentifier` use against either protocol anywhere in the tree, and
every existing conformer is a class, which conforms unchanged. Dropping the
requirement lets a host conform with a value type capturing only what the
work needs — the shape that avoids the cycle, and the one a class-bound
`Delegate` discouraged.

This does not make the cycle impossible: a struct that stores the view
controller cycles just the same. The docs say so rather than implying the
type system settles it.
The closure form of `start` can't capture the object that owns the
server: the closure has to exist before the server does, and retrofitting
`self` would form `owner -> HTTPServer -> handler -> owner`, so the
owner's deinit — and its `stop()` — would never run. A consumer with
dependencies to hold therefore ends up with static functions threading a
context parameter through every call, which is how MediaUploadServer is
written today.

Add an `HTTPRequestHandler` protocol and a `start` overload that takes
one. The dependencies become stored properties and the request logic
becomes instance methods. The protocol is deliberately not
`AnyObject`-constrained: a struct conformer cannot participate in a
reference cycle at all, so the ownership question doesn't arise. A final
class works too, under the same leaf discipline HTTPServerDelegate
already documents.

The closure overload is unchanged and forwards to the same code path, so
this is purely additive — no existing caller, test, or the debug server
is affected. Request handling is mandatory, so it can't be a defaulted
HTTPServerDelegate method the way optional customization points are;
hence an overload rather than a new delegate requirement.
`MediaUploadServer` handled requests through static functions threading an
`UploadContext` parameter through every call, because the closure form of
`HTTPServer.start` can't capture the object that owns the server: the
closure has to exist before the server does, and capturing `self` would
form `MediaUploadServer -> HTTPServer -> handler -> MediaUploadServer`, so
`deinit` — and its `stop()` — would never run.

The previous commit added `HTTPRequestHandler` for exactly this. The
dependencies become stored properties on a `Handler` struct and the
request logic becomes instance methods; a value type can't participate in
a reference cycle, so the ownership question doesn't arise.

Mechanically: `handleRequest` becomes `handle`, the functions that use the
dependencies become instance methods, and the ones that don't
(`attachmentId`, `relayResponse`, `uploadErrorResponse`, `formFields`)
stay static. `UploadContext` goes away — `Handler` is what it was. Helpers
outside the handler (`errorResponse`, `writeStream`, `sanitizeFilename`,
`uploadsTempDirectory`) are qualified rather than moved.

No behavior change: only this file is touched, and no test changed.
Setting a `mediaUploader` means the host is taking over uploads. With no
site credentials the server would previously just not start, silently
dropping the uploader — and its media deletes still need the internal
media client to reach the configured site, since every attachment lives
there no matter who delivered it. Starting anyway would give a server
whose every delete 500s.

So the behavior forks by intent. A `mediaProcessor` with no credentials
leaves the server down and uploads fall to the default WebView path —
there is nothing to deliver through, so nothing to process. A
`mediaUploader` with no credentials is a configuration error and fails
fast: `precondition` on iOS, `check` on Android.

The iOS policy lives in `MediaServerCredentials` rather than
`EditorViewController`, which is `#if canImport(UIKit)` and therefore
absent from the macOS host — the one platform that can run Swift Testing's
exit tests. Living outside the gate, the trap itself is testable, not just
the predicate: two exit tests run it in a child process, and neutering the
precondition fails both. Android's `check` is covered through
`GutenbergView`, and neutering it fails those two as well.
@jkmassel
jkmassel force-pushed the fix/media-uploader-credentials-trap branch from 345809f to 16a9557 Compare September 16, 2026 19:41
@jkmassel jkmassel closed this Sep 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Android iOS [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants