Skip to content

REST API: Always register the media creation arguments - #12833

Closed
adamsilverstein wants to merge 6 commits into
WordPress:trunkfrom
adamsilverstein:fix/65517-register-url-arg
Closed

REST API: Always register the media creation arguments#12833
adamsilverstein wants to merge 6 commits into
WordPress:trunkfrom
adamsilverstein:fix/65517-register-url-arg

Conversation

@adamsilverstein

@adamsilverstein adamsilverstein commented Aug 4, 2026

Copy link
Copy Markdown
Member

Addresses @jeremyfelt's question in https://core.trac.wordpress.org/ticket/65517#comment:14 about url, generate_sub_sizes, and convert_format being processed on POST /wp/v2/media even when they aren't registered in the schema.

All three are now registered unconditionally.

Worth being clear that leaving them unregistered was never disabling them. create_item() reads the parameters either way, so they already worked on sites without client side media processing, just without the validation and sanitization their registration carries. An unsafe sideload url came back as a bare http_request_failed instead of a 400. Registering them always is what actually closes that.

Registering conditionally also made the schema depend on request context rather than site configuration - wp_is_client_side_media_processing_enabled() is derived from is_ssl() and the host, so the same site could advertise different arguments over http and https, or when reached behind a proxy that doesn't set HTTPS. Thanks @andrewserong for pushing on this.

url doesn't depend on the feature: sideloading an external image works around a cross-origin fetch the browser can't make, and that fails whether or not client side media processing is enabled. generate_sub_sizes and convert_format don't either - skipping sub-size generation or format conversion is something the server can do on its own, and sub-sizes stay recoverable through wp_update_image_subsizes().

One condition is kept: generate_sub_sizes of false no longer relaxes the unsupported image type check in create_item_permissions_check() unless client side media processing is enabled. That check exists because the server can't process the image, so it should only be relaxed when something else can. Otherwise the upload is stored unprocessable.

Trac ticket: https://core.trac.wordpress.org/ticket/65808

How has this been tested

npm run test:php -- --filter 'WP_Test_REST_Attachments_Controller' --group restapi
  • Four new tests with client side media processing disabled: all three arguments are registered, sideloading works, an unsafe URL is rejected with a 400, generate_sub_sizes of false skips sub-sizes, and the unsupported image type check still applies. Most fail on trunk without the change - I checked by stashing the source change and re-running.
  • Full WP_Test_REST_Attachments_Controller class: 195 tests, 2 skipped, 0 failures.
  • phpcs --standard=phpcs.xml.dist clean on both changed files.
  • wp-api-generated.js needs no update. The fixture is generated with client side media processing enabled, and in that configuration the registered arguments are unchanged.

Types of changes

  • Register url, generate_sub_sizes, and convert_format unconditionally in get_endpoint_args_for_item_schema().
  • Keep the unsupported image type check in create_item_permissions_check() from being relaxed unless client side media processing is enabled.
  • Add tests, and a disable_client_side_media_processing() test helper.

Note on an existing test

test_upload_unsupported_image_type_skipped_when_not_generating_sub_sizes (from #64836) called the permissions check with generate_sub_sizes of false without enabling client side media processing, so it was asserting exactly the behavior this changes. It now enables the feature first, which keeps its original intent - the comment on it already says "when the client handles image processing" - but flagging it since it's an existing test from another ticket. @jeremyfelt @andrewserong does that read right to you?

Use of AI Tools

AI assistance: Yes
Tool(s): Claude Code
Model(s): Claude Opus 5
Used for: Drafting the implementation and tests from the review notes on the ticket. I reviewed and verified the behavior and test results myself.

Commit message

REST API: Always register the media creation arguments.

The `url`, `generate_sub_sizes`, and `convert_format` arguments for `POST /wp/v2/media` were only registered when client side media processing is enabled, but `create_item()` and `create_item_permissions_check()` honored all three either way. Since an unregistered argument skips the validation and sanitization its registration carries, an unsafe sideload `url` failed with a bare `http_request_failed` instead of a 400. Gating registration also made the schema depend on request context rather than site configuration, since `wp_is_client_side_media_processing_enabled()` is derived from `is_ssl()` and the host, so the same site could advertise different arguments depending on how it was reached.

All three arguments are now registered unconditionally. None of them require the feature: sideloading from a URL works around a cross-origin fetch the browser cannot make, and skipping sub-size generation or format conversion is something the server can do on its own. One condition is kept: `generate_sub_sizes` of `false` no longer relaxes the unsupported image type check in `create_item_permissions_check()` unless client side media processing is enabled, since that check exists because the server cannot process the image and should only be relaxed when the client can. Behavior with client side media processing enabled is unchanged.

Follow-up to [62659], [62841].

Props andrewserong, jeremyfelt.
Fixes #65808. See #65517.

Sideloading an external image with the `url` parameter on `POST /wp/v2/media` works around a cross-origin fetch the browser cannot make. That fetch fails regardless of whether client-side media processing is enabled, so the argument was registered too narrowly.

Leaving the argument unregistered did not disable it. `create_item()` reads the parameter either way, so on a site without client-side media processing the sideload still ran, but without the `sanitize_url` and `wp_http_validate_url()` callbacks the registered argument carries, and an unsafe URL returned a bare `http_request_failed` rather than a 400. Register `url` unconditionally so its validation always applies.

The `generate_sub_sizes` and `convert_format` parameters are different: they hand image processing to the client, and the route the client uploads the results to is only registered when the feature is enabled. Honoring them otherwise would leave an attachment with no sub-sizes and no way to add them, and would relax the unsupported image type check in `create_item_permissions_check()` on a site that never opted in. Both are now ignored unless client-side media processing is enabled.

Follow-up to [62659].

See #65517.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props adamsilverstein, andrewserong, jeremyfelt.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@andrewserong

Copy link
Copy Markdown
Contributor

Ignore generate_sub_sizes and convert_format in create_item() and create_item_permissions_check() unless client side media processing is enabled.

the comment on it already says "when the client handles image processing" - but flagging it since it's an existing test from another ticket. @jeremyfelt @andrewserong does that read right to you?

I'm a little on the fence about this one. In terms of the REST API itself, for the behaviours of switching off conversion or generating sub-sizes, do we care if client-side media processing is enabled? Is whether client-side media processing is enabled orthogonal to the behaviour here?

I don't mind too much either way, I'm mostly thinking about how to keep things simple.

@jeremyfelt

Copy link
Copy Markdown
Member

@adamsilverstein @andrewserong 👋🏻 Sorry for the late flag, I'm still wrapping my head around bits here. Here's my understanding of the paths that we're adding in 7.1.

First, there is a new "Upload to Media Library" button in the editor that appears when you have image block markup like this:

<!-- wp:image {"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="https://example.test/external/1234.jpg"></figure>
<!-- /wp:image -->

This submits a payload to the wp/v2/media endpoint with post and url attributes. It does not attempt to do anything with client side media processing.

This can also be fired as part of a pre-publish check suggesting that external media be uploaded.

Second, when a supported image is uploaded by the user in a supported browser and true === wp_is_client_side_media_processing_enabled(), the resizing is handled in the browser and associated with multiple requests:

  1. One request to the wp/v2/media endpoint with file (image binary), post (post ID), and generate_sub_sizes (false) attributes.
  2. Multiple requests to the wp/v2/media/<id>/sideload endpoint with file (image binary), image_size, and convert_format (false) attributes.
  3. One request to the wp/v2/media/<id>/finalize endpoint with sub_sizes as an array of data about the generated image sizes.

And then third, as a side effect of adding those features, the ability for extenders to make calls to these endpoints:

  1. wp/v2/media with the url attribute (and generate_sub_sizes if client side processing enabled)
  2. wp/v2/media/<id>/sideload (if client side processing enabled)
  3. wp/v2/media/<id>/finalize (if client side processing enabled)

Conclusions

It seems like the WordPress core editor side of things is working as expected, but I think there are a handful of other things that can be cleaned up or clarified before folks discover the endpoints and start using them. It will be harder to correct in the future and maintain back-compat.

  1. Return an error if both file and url attributes are sent to wp/v2/media at the same time.
  2. Ensure fields like title, alt_text, caption, etc.... work when a url attribute is provided.
  3. Fire rest_pre_insert_attachment and rest_insert_attachment for requests where url attribute is provided. (And add a check for is_wp_error() on the pre-hook.)
  4. Call wp_after_insert_post() to fire wp_after_insert_post. (A comment in create_item_from_url() says it's handled, but attachments are returned too early in wp_insert_post())

Items 2, 3, and 4 come from a lack of parity between create_item() and create_item_from_url(). It may be helpful to merge those methods—or rather than return early, continue with create_item() after create_item_from_url() handles the initial sideload.

@adamsilverstein

Copy link
Copy Markdown
Member Author

Items 2, 3, and 4 come from a lack of parity between create_item() and create_item_from_url(). It may be helpful to merge those methods—or rather than return early, continue with create_item() after create_item_from_url() handles the initial sideload.

That makes sense, we would want the usual create_item pathway to fire as usual, as if the user had upload the file directly.

* registered otherwise, so a site that has not opted in should not have
* this check relaxed by an unrecognized parameter.
*/
if ( wp_is_client_side_media_processing_enabled() && false === $request['generate_sub_sizes'] ) {

@andrewserong andrewserong Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if my earlier question got lost: but why do we guard generate_sub_sizes and convert_format behind the presence of client side media processing? Should we register these unconditionally, too?

(Not a blocker, you've already explained part of the reasoning in the code comments. My only hesitation was really if it's a strange thing for a REST API to conditionally have some args available only some of the time)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right, and thanks for asking twice - I've registered all three unconditionally now.

The thing that convinced me is that wp_is_client_side_media_processing_enabled() comes from is_ssl() and the host, so gating registration on it means the same site advertises different args over http vs https, or behind a proxy that doesn't set HTTPS. That's request context leaking into the schema, and since an unregistered param is still readable we were duplicating the check at runtime anyway.

I did keep one guard: generate_sub_sizes of false no longer relaxes the unsupported image type check in create_item_permissions_check() unless client side media processing is enabled. That check is there because the server can't process the image, so it seems worth only relaxing when the client can - otherwise we store something nothing can process. Skipping sub-sizes or conversion is fine either way, and they can still be regenerated with wp_update_image_subsizes().

Does that split make sense to you?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that split makes sense to me! And thanks for digging in further there, that's exactly the nuance I was wondering about, but unsure of 😄

The comment described why an earlier trunk iteration changed rather than
documenting the feature, which read as out of place for new 7.1 code. The
registration stands on its own.

See WordPress#12846 (comment)
Gating `generate_sub_sizes` and `convert_format` on client side media
processing made the schema depend on request context rather than site
configuration, since the feature is derived from the scheme and host. The
same site could advertise different arguments over http and https, or when
reached behind a proxy that does not set HTTPS.

Both arguments are instructions the server can carry out on its own -
skipping sub sizes or format conversion needs no client - so they are now
registered and honored either way, and the runtime checks that duplicated
the registration condition are gone. The one condition kept is the
unsupported image type check in the permissions callback: that check exists
because the server cannot process the image, so it is only relaxed when
client side media processing means something else can.

Sub sizes skipped this way remain recoverable through
wp_update_image_subsizes(), and behavior with client side media processing
enabled is unchanged.
@adamsilverstein adamsilverstein changed the title REST API: Always register the media url argument REST API: Always register the media creation arguments Aug 5, 2026

@andrewserong andrewserong left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good to me, and I see you're already following up on the other notes and ideas in a separate PR 👍

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown

A commit was made that fixes the Trac ticket referenced in the description of this pull request.

SVN changeset: 63018
GitHub commit: 6904e89

This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.

@github-actions github-actions Bot closed this Aug 5, 2026
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.

3 participants