Conversation
4 tasks done
XCFramework BuildThis PR's XCFramework is available for testing. Add the following to your .package(url: "https://github.com/wordpress-mobile/GutenbergKit", branch: "pr-build/632")Built from 16a9557 |
jkmassel
force-pushed
the
fix/media-uploader-credentials-trap
branch
from
September 8, 2026 15:15
9ee12ea to
eef9925
Compare
jkmassel
force-pushed
the
fix/media-uploader-credentials-trap
branch
from
September 8, 2026 16:12
eef9925 to
873c04d
Compare
jkmassel
force-pushed
the
fix/media-uploader-credentials-trap
branch
from
September 9, 2026 00:56
873c04d to
8506a8b
Compare
5 tasks done
jkmassel
force-pushed
the
fix/media-uploader-credentials-trap
branch
from
September 15, 2026 22:39
8506a8b to
345809f
Compare
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
force-pushed
the
fix/media-uploader-credentials-trap
branch
from
September 16, 2026 19:41
345809f to
16a9557
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #631. Ninth of ten PRs splitting #621.
What?
Setting a
mediaUploaderwithout site credentials is a configuration error, and now fails fast instead of silently doing nothing.Why?
Setting a
mediaUploadermeans 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:
mediaProcessorwith no credentials leaves the server down and uploads fall to the default WebView path — there is nothing to deliver through, so nothing to process.mediaUploaderwith no credentials traps:preconditionon iOS,checkon Android.The iOS policy lives in
MediaServerCredentials(added in #624) rather thanEditorViewController, 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
preconditionfails bothGutenbergView— the two uploader arms and the processor arm; neutering thecheckfails the two uploader testsswift test— host suite green:Gutenberg:testDebugUnitTestgreenxcodebuild