Support the ActivityPub Media Upload extension#927
Conversation
✅ Deploy Preview for fedify-json-schema canceled.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis PR adds ActivityPub media upload support across federation APIs, request handling, vocab, lint rules, tests, docs, and changelog entries. It introduces the media upload endpoint, URI helpers, endpoint advertisement, and validation around callback and actor configuration. ChangesActivityPub Media Upload feature
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
@codex review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for the ActivityPub Media Upload extension, allowing servers to accept client-to-server media uploads. It adds the setMediaUploader method to register a multipart/form-data upload endpoint, updates the Context to build the endpoint URI, and exposes the Endpoints.uploadMedia property. Additionally, three new lint rules are added to @fedify/lint to validate correct configuration. A review comment suggests adding a defensive check in the request handler to prevent a potential runtime crash if the media uploader callback returns a nullish value.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b6784f2edb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/fedify/src/federation/handler.ts`:
- Around line 1069-1081: The multipart parsing in handler.ts via
request.formData() has no built-in size or timeout cap, so add a documented
recommendation or configuration hook for deployers to enforce request-size
limits at the reverse-proxy/runtime layer. Update the handling around the
request.formData() path in the federation handler to make the limitation
explicit, and if possible expose a knob or note near the multipart parsing logic
so operators can bound uploads before they reach request.formData().
- Around line 1164-1176: The `handleMediaUpload` success path is missing error
handling around `result.toJsonLd(ctx)`, so a serialization/context-loader
failure escapes uncaught. Wrap the `toJsonLd` call in the same guarded
error-to-response flow used elsewhere in `handler.ts` (the multipart parse,
field validation, and callback failure paths), and convert any thrown exception
into a controlled 500-style response instead of letting it propagate through
`FederationImpl.fetch()`.
In `@packages/fedify/src/federation/middleware.test.ts`:
- Around line 3889-3914: The upload test helper only covers the `object` field
as a `Blob`, so the `handleMediaUpload` plain-string branch is untested. Add a
dedicated test in `middleware.test.ts` that builds a `FormData` with `file` plus
`object` appended as a raw JSON string, then calls the federation upload
endpoint and asserts the request is accepted. Use the existing
`createFederation`, `setActorDispatcher`, and `setMediaUploader` setup to place
the test near the current media upload coverage and explicitly exercise `typeof
objectField === "string"`.
In `@packages/lint/src/index.ts`:
- Around line 66-77: The new rule exports in the lint index are wired into rules
but are not enabled by the recommended config yet. Update recommendedRuleIds in
the index module to include the new rule identifiers alongside actorIdRequired
and actorIdMismatch if they should be part of the recommended preset, and verify
the symbol names match the imported rule modules such as
actorUploadMediaPropertyMismatch, actorUploadMediaPropertyRequired,
collectionFiltering, and mediaUploaderObjectUriRequired.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 517bf533-09de-4a78-bf40-c9661f49ede7
⛔ Files ignored due to path filters (4)
packages/vocab-tools/src/__snapshots__/class.test.ts.deno.snapis excluded by!**/*.snappackages/vocab-tools/src/__snapshots__/class.test.ts.node.snapis excluded by!**/*.snappackages/vocab-tools/src/__snapshots__/class.test.ts.snapis excluded by!**/*.snappackages/vocab/src/__snapshots__/vocab.test.ts.snapis excluded by!**/*.snap
📒 Files selected for processing (31)
CHANGES.mddocs/.vitepress/config.mtsdocs/manual/actor.mddocs/manual/context.mddocs/manual/lint.mddocs/manual/media-upload.mddocs/manual/opentelemetry.mdpackages/fedify/src/federation/builder.test.tspackages/fedify/src/federation/builder.tspackages/fedify/src/federation/callback.tspackages/fedify/src/federation/context.tspackages/fedify/src/federation/federation.tspackages/fedify/src/federation/handler.tspackages/fedify/src/federation/middleware.test.tspackages/fedify/src/federation/middleware.tspackages/fedify/src/testing/context.tspackages/lint/README.mdpackages/lint/src/index.tspackages/lint/src/lib/const.tspackages/lint/src/mod.tspackages/lint/src/oxlint.tspackages/lint/src/rules/actor-upload-media-property-mismatch.tspackages/lint/src/rules/actor-upload-media-property-required.tspackages/lint/src/rules/media-uploader-object-uri-required.tspackages/lint/src/tests/actor-upload-media-property-mismatch.test.tspackages/lint/src/tests/actor-upload-media-property-required.test.tspackages/lint/src/tests/media-uploader-object-uri-required.test.tspackages/testing/src/context.tspackages/testing/src/mock.tspackages/vocab/src/endpoints.yamlpackages/vocab/src/vocab.test.ts
The media upload handler converted every other failure (multipart parse, missing fields, callback throw) into a controlled response, but two paths could still crash or leak an exception. The callback's return type now permits a plain, non-Promise value, so a JavaScript caller or a callback with no return statement can yield null or undefined; reading result.id on such a value would throw and take down the request. It now returns a 500 instead of crashing. Serializing the returned object with toJsonLd() was unguarded, so a context-loader failure would escape out of Federation.fetch() (which re-throws after logging) as an unhandled rejection rather than a clean 500 response. It is now wrapped like every other failure mode in the handler. Also added a test for the plain-string object form field, whose branch was previously never exercised because the test helper always sends the field as a Blob. fedify-dev#927 (comment) fedify-dev#927 (comment) fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
MediaUploaderCallback permits returning Object, URL, or a Promise of either, so a synchronous callback may legitimately wrap its result in Promise.resolve(...). The media-uploader-object-uri-required rule only recognized a direct getObjectUri() call, so such a callback was warned about under the recommended lint configuration despite being valid. unwrapExpression() now strips a single-argument Promise.resolve(...) wrapper, alongside await and type assertions, before judging the returned value, so both the URL and object-id forms are accepted whether or not they are wrapped in a resolved promise. fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
|
@codex review |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request adds support for the ActivityPub Media Upload extension, allowing servers to accept client-to-server media uploads. It introduces the setMediaUploader method to register an upload endpoint, the getMediaUploaderUri method to build the endpoint URI, and a new media_upload metric category. Additionally, three new lint rules are added to @fedify/lint to ensure proper configuration of the media upload endpoint. Feedback on the changes suggests enhancing the unwrapExpression helper in the media-uploader-object-uri-required lint rule to support TSTypeAssertion and ParenthesizedExpression nodes, preventing potential false positives.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a5871f056
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
unwrapExpression() in the media-uploader-object-uri-required rule stripped the as, non-null, and satisfies assertions but not the angle-bracket type assertion (<T>expr) nor a parenthesized expression, so a returned value wrapped in either was not recognized as deriving from getObjectUri() and produced a false positive. fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
When an authorize hook was configured, the media upload handler cloned the request to hand a copy to the hook. Cloning a request tees its body stream, so once the handler read the original with request.formData(), the entire uploaded file was buffered a second time for the authorize clone that a header/token hook never consumes; a large upload could therefore double its peak memory footprint. The hook now receives a header-only request built from the original's URL, method, and headers, so the body is never teed. On rejection the original request (still unread, since formData() runs only after authorization succeeds) is handed to onUnauthorized intact. fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
|
@codex review |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request adds support for the ActivityPub Media Upload extension, enabling servers to accept client-to-server media uploads. It introduces setMediaUploader() to register the endpoint, getMediaUploaderUri() to retrieve its URI, and exposes the Endpoints.uploadMedia property. Additionally, three new lint rules are added to @fedify/lint to ensure correct configuration. Feedback on the changes suggests adding a defensive check in the request handler to ensure that the parsed JSON-LD object is not null before passing it to the callback, preventing potential runtime TypeErrors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dc80c9d5cf
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The media-uploader-object-uri-required rule accepted a bare object
literal return, such as () => ({ id: ctx.getObjectUri(...) }), as valid.
But the handler calls toJsonLd() on the result and returns a 500 when it
is missing, so a plain object is never a valid media uploader result.
The rule now only accepts a vocab object built with a constructor
(new X({...})) or a URL; a plain object literal falls through to the URL
check and is flagged.
fedify-dev#927 (comment)
Assisted-by: Claude Code:claude-opus-4-8
|
@codex review |
|
@codex review |
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request introduces support for the ActivityPub Media Upload extension, enabling servers to accept client-to-server media uploads. Key changes include adding the setMediaUploader() method to Federation and FederationBuilder to register a multipart/form-data upload endpoint, and adding getMediaUploaderUri() to Context to build the endpoint URI advertised under Endpoints.uploadMedia. Additionally, three new lint rules (media-uploader-object-uri-required, actor-upload-media-property-required, and actor-upload-media-property-mismatch) have been added to @fedify/lint to ensure correct usage. Comprehensive tests, documentation, and vocabulary updates have also been included. There are no review comments, and I have no feedback to provide.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
Codex Review: Didn't find any major issues. Can't wait for the next one! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
sij411
left a comment
There was a problem hiding this comment.
Lint-rule edge case:
media-uploader-object-uri-required treats destructured getObjectUri calls as valid, e.g. async ({ getObjectUri }) => getObjectUri(...). However, the real ContextImpl. getObjectUri() method reads this.federation / this.canonicalOrigin, so destructuring the method loses its receiver and should throw at runtime. Unless these context methods are intentionally safe to call unbound, this rule may produce a falsse negative and the test case for destructured getObjectUri may be encouraging broken code. would it be safer to only accept ctx.getObjectUri(...) member calls on the callback's context parameter?
The media-uploader-object-uri-required rule accepted a getObjectUri call
destructured off the context parameter, as in
async ({ getObjectUri }) => getObjectUri(...). But Context.getObjectUri()
reads this.federation and this.canonicalOrigin, so destructuring it loses
the method's receiver and throws a TypeError at runtime; accepting that
form let the rule pass provably broken code, and its test even encouraged
it.
The rule now recognizes only a member call on the callback's context
parameter (ctx.getObjectUri(...)); the destructured form is flagged, and
its test is inverted accordingly. This also drops the alias tracking,
simplifying the rule.
fedify-dev#927 (review)
Assisted-by: Claude Code:claude-opus-4-8
|
@sij411 Good catch, thanks. I confirmed at runtime that destructuring |
Fedify-based servers could accept client-to-server activities through setOutboxListeners() (fedify-dev#430), but had no way to accept the binary media uploads that the client-to-server protocol relies on. This adds support for the ActivityPub Media Upload extension so applications can register a dedicated multipart/form-data upload endpoint. New API: - Federation.setMediaUploader(path, callback) registers the endpoint; the callback finalizes the uploaded file and returns either the created object (201 Created) or the URL at which it will become available (202 Accepted). The returned setter exposes .authorize(). - Context.getMediaUploaderUri(identifier) builds the endpoint URI, which actor dispatchers advertise under the new Endpoints.uploadMedia vocabulary property. The endpoint mirrors the outbox path: it rejects uploads for missing or tombstoned actors before invoking the callback, requires multipart/form-data, and returns 201/202 with a Location header (an object without an id yields 500, since a 201 cannot omit Location). It warns at runtime when a returned id/URL does not point at a registered object dispatcher route, or when a registered uploader is not advertised under endpoints.uploadMedia. @fedify/lint gains three rules: - media-uploader-object-uri-required - actor-upload-media-property-required - actor-upload-media-property-mismatch See fedify-dev#754 for the design discussion. Assisted-by: Claude Code:claude-opus-4-8 Assisted-by: Codex:gpt-5.5
The media upload handler converted every other failure (multipart parse, missing fields, callback throw) into a controlled response, but two paths could still crash or leak an exception. The callback's return type now permits a plain, non-Promise value, so a JavaScript caller or a callback with no return statement can yield null or undefined; reading result.id on such a value would throw and take down the request. It now returns a 500 instead of crashing. Serializing the returned object with toJsonLd() was unguarded, so a context-loader failure would escape out of Federation.fetch() (which re-throws after logging) as an unhandled rejection rather than a clean 500 response. It is now wrapped like every other failure mode in the handler. Also added a test for the plain-string object form field, whose branch was previously never exercised because the test helper always sends the field as a Blob. fedify-dev#927 (comment) fedify-dev#927 (comment) fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
MediaUploaderCallback permits returning Object, URL, or a Promise of either, so a synchronous callback may legitimately wrap its result in Promise.resolve(...). The media-uploader-object-uri-required rule only recognized a direct getObjectUri() call, so such a callback was warned about under the recommended lint configuration despite being valid. unwrapExpression() now strips a single-argument Promise.resolve(...) wrapper, alongside await and type assertions, before judging the returned value, so both the URL and object-id forms are accepted whether or not they are wrapped in a resolved promise. fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
unwrapExpression() in the media-uploader-object-uri-required rule stripped the as, non-null, and satisfies assertions but not the angle-bracket type assertion (<T>expr) nor a parenthesized expression, so a returned value wrapped in either was not recognized as deriving from getObjectUri() and produced a false positive. fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
When an authorize hook was configured, the media upload handler cloned the request to hand a copy to the hook. Cloning a request tees its body stream, so once the handler read the original with request.formData(), the entire uploaded file was buffered a second time for the authorize clone that a header/token hook never consumes; a large upload could therefore double its peak memory footprint. The hook now receives a header-only request built from the original's URL, method, and headers, so the body is never teed. On rejection the original request (still unread, since formData() runs only after authorization succeeds) is handed to onUnauthorized intact. fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
The media-uploader-object-uri-required rule accepted a bare object
literal return, such as () => ({ id: ctx.getObjectUri(...) }), as valid.
But the handler calls toJsonLd() on the result and returns a 500 when it
is missing, so a plain object is never a valid media uploader result.
The rule now only accepts a vocab object built with a constructor
(new X({...})) or a URL; a plain object literal falls through to the URL
check and is flagged.
fedify-dev#927 (comment)
Assisted-by: Claude Code:claude-opus-4-8
The previous change gave the authorize hook a header-only request to avoid teeing the upload body, but that broke signature-based hooks: a hook verifying an RFC 9421 Content-Digest would hash an empty body against the multipart digest header and reject otherwise valid signed uploads. The handler now reads the body once into memory (only when an authorize hook is configured) and rebuilds a fresh Request from those bytes for the hook, for onUnauthorized, and for request.formData(). A signature-verifying hook therefore sees the real body and headers, while the file is still buffered only once rather than teed and buffered a second time for the common token/header hook. fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
Two issues in the media upload authorization path: The actor was resolved after the authorize hook, so an authorize hook that rejects a request for a nonexistent or tombstoned actor returned 401 instead of the documented 404, and the whole multipart body was buffered for a bogus identifier before the cheap actor-existence check. The actor is now resolved first, so a missing actor gets 404 without buffering the body or running authorization. The pre-authorization body read (request.arrayBuffer()) was unguarded, so an aborted or errored upload rejected and escaped handleMediaUpload as an unhandled server error instead of a controlled response. It is now wrapped like request.formData() and converted into a 400. fedify-dev#927 (comment) fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
The media-uploader-object-uri-required rule accepted a getObjectUri call
destructured off the context parameter, as in
async ({ getObjectUri }) => getObjectUri(...). But Context.getObjectUri()
reads this.federation and this.canonicalOrigin, so destructuring it loses
the method's receiver and throws a TypeError at runtime; accepting that
form let the rule pass provably broken code, and its test even encouraged
it.
The rule now recognizes only a member call on the callback's context
parameter (ctx.getObjectUri(...)); the destructured form is flagged, and
its test is inverted accordingly. This also drops the alias tracking,
simplifying the rule.
fedify-dev#927 (review)
Assisted-by: Claude Code:claude-opus-4-8
The media upload handler resolves the actor before authorization, which differs from the outbox (which authorizes first) and means a request for a nonexistent or deleted actor gets 404 rather than 401 even when it would also fail authorization. This is deliberate: it avoids buffering the upload for a bogus identifier and keeps the 404 reliable regardless of the authorize hook, and it leaks nothing new since actor existence is already public via the actor URI. Document the ordering and its consequence in the media upload guide, and clarify the code comment, instead of implying the two handlers share an ordering. The PR description is corrected separately. fedify-dev#927 (comment) Assisted-by: Claude Code:claude-opus-4-8
A media uploader registered without .authorize() accepts uploads from
anyone who can reach the URL, which for an endpoint that stores files is
a serious abuse, denial-of-service, and storage-cost risk. Make that
hard to miss:
- The media upload guide now carries a prominent warning that
.authorize() must be configured in production unless a public upload
endpoint is intended.
- Fedify logs a one-time runtime warning when a media uploader handles
a request without an authorize predicate; the mediaUploader log
category is now documented in the logging guide.
- A new @fedify/lint rule, media-uploader-authorization-required, flags
a setMediaUploader() call that is not protected with .authorize().
fedify-dev#927 (comment)
Assisted-by: Claude Code:claude-opus-4-8
|
@codex review |
|
/gemini review |
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
|
Codex Review: Didn't find any major issues. Another round soon, please! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Client-to-server support arrived in Fedify 2.2 with
setOutboxListeners()(#430), which lets an application accept ActivityPub C2S activities without exposing a bespoke API. But posting an activity is only half of what C2S clients need: they also upload binary media (images, video, audio), and that traffic deliberately does not travel throughPOST /outbox. The ActivityPub Media Upload extension exists precisely to fill this gap, so it is the natural next step now that the posting surface is in place. This implements it (#754).Why it mirrors the outbox
The API intentionally follows the shape that
setOutboxListeners()established, because the two endpoints are the same kind of thing: an authenticated C2S entry point whose server-side behavior is the application's business, not the framework's.setMediaUploader(path, callback)returns a setter with a single.authorize()hook, authorization runs before the callback, and a rejection produces401(overridable through the federation-levelonUnauthorized), exactly as on the outbox. Keeping the surface familiar means anyone who already wired up outbox listeners can reason about media upload without learning a second mental model, and it preserves Fedify's consistent division of labor across C2S: the framework routes and authorizes, the application decides what to store and serve.Why the endpoint is advertised explicitly rather than auto-injected
The endpoint is exposed through a new
Context.getMediaUploaderUri(identifier), and the actor dispatcher places it under the newEndpoints.uploadMediaproperty itself:An earlier design auto-injected
endpoints.uploadMediainto the actor document. It was dropped because it would be inconsistent with the rest of the framework:getInboxUri(),getOutboxUri(), and their siblings only expose URLs, while deciding what actually appears on the actor document is the actor dispatcher's responsibility. Silently writing one endpoint but not the others would be a surprising special case that erodes that rule. Instead, the framework warns at runtime, and@fedify/lintwarns statically, when an uploader is registered but the actor fails to advertise the matching URI: the convention stays uniform, and the omission is still caught.Why the callback returns
Object | URLThe return type encodes the one decision the framework cannot make on the application's behalf: whether the uploaded resource is fetchable immediately or is still being processed. Returning an
Objectmeans “ready now”, so Fedify answers201 CreatedwithLocationpointing at the object'sid; returning aURLmeans “it will live here once processing finishes” (for example after transcoding), so it answers202 AcceptedwithLocationset to that URL. This is purely the HTTP-level distinction between201and202, independent of whether the callback happens to be asynchronous. The type therefore accepts a plain value or aPromise, matching how every other value-returning Fedify callback behaves, rather than forcing aPromisereturn just because the realistic implementation awaits storage.Why missing actors and
id-less results are rejectedTwo failure modes are closed off before they can turn into a misleading success. First, the handler resolves the actor dispatcher up front and returns
404, without ever invoking the callback, when the actor is absent or aTombstone. The actor dispatcher is Fedify's source of truth for which actors exist, just as on the outbox path; without this guard an application could store media and return201/202for a user that never existed or was deleted. Second, anObjectreturned without anidproduces500instead of a201that lacksLocation. A201 Createdwith noLocationis unusable to the client (it cannot discover the object it just created), so failing loudly is strictly better than succeeding uselessly.How the request is handled
The endpoint is registered as a
mediaUploaderroute and handled before JSON-LD content negotiation, because amultipart/form-dataupload has nothing to negotiate; this placement also lets any non-POSTmethod resolve cleanly to405instead of a confusing negotiation406. From there the handler runs body-state guards, a content-type check, actor resolution, authorization, parsing, and the callback. It deliberately resolves the actor before authorization, unlike the outbox: a request for a nonexistent or deleted actor receives404regardless of the authorize hook, and the upload body is not buffered for a bogus identifier (the actor's existence is already public via its actor URI, so this leaks nothing new). A newmedia_uploadvalue in thefedify.endpointmetric attribute keeps these requests distinguishable in telemetry rather than lumping them intonot_found.Why the linting is shaped the way it is
Runtime warnings only fire once the offending code path actually executes, so the three new
@fedify/lintrules move the same guarantees to development time. The two actor-side rules (actor-upload-media-property-requiredandactor-upload-media-property-mismatch) reuse the existing property-checking machinery and therefore behave like the establishedsharedInboxrules. The object-side rule (media-uploader-object-uri-required) is where the real care went, because “the returned id must derive fromgetObjectUri()“ is easy to state and easy to get subtly wrong. It inspects the returned expressions specifically, not the whole callback body, so agetObjectUri()call sitting in a dead branch cannot excuse a hard-coded return. For object returns it judges the effectiveidalone (the lastidproperty wins, and a trailing spread is treated as unsafe), so agetObjectUri()call in some unrelated property such asurlcannot launder a hard-codedid. And it requires the call's receiver to be the callback's own context parameter, sohelper.getObjectUri(...)on some other object is not mistaken for the genuine one.What is intentionally left to the application
Consistent with the philosophy adopted in #430, several responsibilities stay with the developer: storing the file, serving the uploaded object back as fetchable ActivityStreams (that is what
setObjectDispatcher()is for), wrapping the result in aCreate, and capping the upload size. On that last point, Fedify reads the multipart body into memory and imposes no size limit of its own, the same as the inbox and outbox. Rather than bolt on a one-off limit option, the documentation directs applications to enforce a maximum at the deployment layer (for example a reverse proxy's body-size setting), which is where Fedify already expects that boundary to sit.Documentation
A new docs/manual/media-upload.md walks through the whole flow (registering the uploader, the
201/202semantics, advertising the endpoint, and the error responses), andgetMediaUploaderUri(), theendpoints.uploadMediaproperty, and the three lint rules are documented next to their existing siblings.