Skip to content

Connectors: don't clobber third-party custom render in registerDefaultConnectors#77116

Open
superdav42 wants to merge 1 commit intoWordPress:trunkfrom
superdav42:fix/connectors-default-render-clobber
Open

Connectors: don't clobber third-party custom render in registerDefaultConnectors#77116
superdav42 wants to merge 1 commit intoWordPress:trunkfrom
superdav42:fix/connectors-default-render-clobber

Conversation

@superdav42
Copy link
Copy Markdown

What

registerDefaultConnectors() in routes/connectors-home/default-connectors.tsx was unconditionally setting args.render = ApiKeyConnector for any api_key-authenticated connector, overwriting custom renders that third-party plugin script modules had already supplied via __experimentalRegisterConnector().

This PR makes the assignment conditional on no existing render being registered, so plugin-supplied renders survive while the rest of the server-side metadata (logo, plugin install state, authentication config) still merges on top.

Why

registerDefaultConnectors() runs inside the dynamically-imported routes/connectors-home/content module. By the time it runs, every top-level plugin script module — including any that enqueued on the standard options-connectors-wp-admin_init action — has already executed and registered its connectors. The store reducer spreads new config over existing entries:

[ action.slug ]: {
    ...state.connectors[ action.slug ],   // existing
    slug: action.slug,
    ...action.config,                      // new (wins per key)
}

So whichever caller fires last wins per key, and registerDefaultConnectors() is always last for plugins that enqueue at the standard hook. The result is that any plugin shipping a custom Connectors-page card sees it silently replaced by the generic ApiKeyConnector API-key form.

This is particularly painful for local/self-hosted AI providers (Ollama, LM Studio, WebLLM, etc.) that don't have an API-key concept at all — the rendered form is meaningless and confusing for users.

PR #76722 added JS-extensibility e2e coverage but only for the server-then-client direction (where the test plugin's connector uses auth: 'none', so the clobber path is never hit). The client-then-server direction for auth: 'api_key' connectors was unverified and broken — see issue #77115 for the full root-cause walkthrough and a live reproducer plugin.

How

In default-connectors.tsx, before deciding whether to assign the default ApiKeyConnector render, read the current entry from the connectors store via the same unlock( connectorsPrivateApis ) pattern stage.tsx already uses:

const existing =
    unlock( select( connectorsStore ) ).getConnector( connectorName );
if ( authentication.method === 'api_key' && ! existing?.render ) {
    args.render = ApiKeyConnector;
}

When args.render is omitted, the reducer's spread of the existing entry preserves whatever render the plugin already registered. Server-side metadata (name, description, logo, authentication, plugin) still merges on top of the existing entry as before.

Test plan

Adds a third connector test_api_key_with_custom_render to the connectors-js-extensibility test plugin:

  • PHP (packages/e2e-tests/plugins/connectors-js-extensibility.php): registers test_api_key_with_custom_render with type: 'ai_provider' and authentication.method: 'api_key' — exactly the path that hits the clobber.
  • JS (packages/e2e-tests/plugins/connectors-js-extensibility/index.mjs): calls registerConnector( 'test_api_key_with_custom_render', { render: ... } ) for it. This is what registerDefaultConnectors() would have overwritten without the fix.
  • Spec (test/e2e/specs/admin/connectors.spec.js): a new test inside the JS extensibility describe block:
    • Asserts the custom render's text content ('Custom render survived registerDefaultConnectors().') is visible inside the expected card.
    • Asserts the card has zero API Key labels — i.e. the default ApiKeyConnector form is absent.

This explicitly exercises the previously-uncovered direction. Without the source fix, the new spec fails (the card shows the API Key form instead of the custom content).

  • Existing JS extensibility tests still pass.
  • New should preserve a custom render for an api_key connector... test passes.
  • Manual smoke: install any third-party AI provider plugin that registers a custom connector card (e.g. https://github.com/Ultimate-Multisite/ultimate-ai-connector-webllm) and verify the custom card renders without needing the multi-tick registerConnector workaround.

Closes #77115.

…tConnectors

`registerDefaultConnectors()` runs inside the dynamically-imported
`routes/connectors-home/content` module, which means it executes after any
third-party plugin script module that called
`__experimentalRegisterConnector()` at top level (e.g. plugins enqueueing on
`options-connectors-wp-admin_init`). The connectors store reducer spreads
the new config over the existing entry, so the unconditional
`args.render = ApiKeyConnector` for `api_key`-authenticated providers
overwrites any custom render the plugin already supplied.

PR WordPress#76722 added e2e coverage for the *server-then-client* direction (where
the JS register correctly wins because it runs after the default register),
but didn't cover the *client-then-server* direction — which is the natural
flow for plugin script modules.

Fix: read the existing entry from the connectors store and only set
`args.render = ApiKeyConnector` when no render has been registered yet.
The reducer's spread of the existing entry preserves the plugin's render
while still merging the server-side metadata (logo, plugin install state,
authentication config) on top.

Adds a third connector `test_api_key_with_custom_render` to the
`connectors-js-extensibility` test plugin and a corresponding e2e test that
asserts the custom render content is visible and the default API Key form
is absent — exercising the previously-uncovered direction.

Closes WordPress#77115
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

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.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: superdav42 <superdav42@git.wordpress.org>

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 7, 2026

👋 Thanks for your first Pull Request and for helping build the future of Gutenberg and WordPress, @superdav42! In case you missed it, we'd love to have you join us in our Slack community.

If you want to learn more about WordPress development in general, check out the Core Handbook full of helpful information.

@github-actions github-actions bot added the First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository label Apr 7, 2026
@superdav42
Copy link
Copy Markdown
Author

I don't have permission to apply labels — could a maintainer add [Type] Bug so the required-labels check passes? Thanks!

superdav42 added a commit to Ultimate-Multisite/ultimate-ai-connector-compatible-endpoints that referenced this pull request Apr 7, 2026
WP core's routes/connectors-home/content.js runs registerDefaultConnectors()
from inside an async dynamic import. By the time it executes, our top-level
registerConnector() has already run, and the connectors store reducer
spreads new config over the existing entry — so the default's
`args.render = ApiKeyConnector` overwrites our custom card. The user sees
the generic API-key UI instead of the endpoint URL / model picker form.

The proper upstream fix is WordPress/gutenberg#77116. Until that lands and
ships in a Gutenberg release, work around it by re-asserting our
registration on five ticks (sync + microtask + setTimeout 0/50/250/1000ms)
so we always end up last regardless of dynamic-import resolution order.

Re-registration with the same render is idempotent so the redundant calls
cost almost nothing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
superdav42 added a commit to Ultimate-Multisite/ai-provider-for-anthropic-max that referenced this pull request Apr 7, 2026
WP core's routes/connectors-home/content.js runs registerDefaultConnectors()
from inside an async dynamic import, and the connectors store reducer
spreads new config over existing entries — so the default's
`args.render = ApiKeyConnector` can overwrite a plugin's custom render.

This plugin currently dodges the bug because its JS slug
(ai-provider-for-anthropic-max/connector) doesn't collide with the PHP-side
provider id (anthropic-max). That's fragile — a future upstream change to
slug normalization or sanitization could break it without warning.

Re-assert the registration on five ticks (sync + microtask + setTimeout
0/50/250/1000ms) so we always end up last regardless of dynamic-import
resolution order. Idempotent and cheap.

The proper upstream fix is WordPress/gutenberg#77116. Once that ships in
a Gutenberg release, this defense can be removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
superdav42 added a commit to Ultimate-Multisite/ai-provider-for-anthropic-max that referenced this pull request Apr 7, 2026
…ix render clobber

This commit makes two coordinated changes that have to ship together:

1. RENAME the AI Client provider id from 'anthropic-max' to
   'ultimate-ai-connector-anthropic-max', and the JS registerConnector()
   slug from 'ai-provider-for-anthropic-max/connector' to the same
   'ultimate-ai-connector-anthropic-max'. This:

   - Matches the naming convention used by the sister plugins
     (ultimate-ai-connector-webllm, ultimate-ai-connector-compatible-endpoints).
   - Claims the 'ultimate-ai-connector-' namespace properly so a future
     third-party 'anthropic-max' plugin can't collide with this one.
   - Causes the WP Connectors page to render ONE card instead of two —
     previously a hidden duplicate auto-registered ApiKeyConnector card
     existed alongside the custom-rendered card because the slugs differed.

   This is a BREAKING change for any caller that hardcoded the old
   provider id (e.g. AiClient::defaultRegistry()->getProvider('anthropic-max')).
   Stored OAuth tokens, REST endpoint URLs, option keys, transient prefixes,
   plugin folder name, text domain, and css class are unchanged.

2. WORKAROUND for WP core's registerDefaultConnectors() clobbering custom
   renders. The rename in (1) makes the slugs match, which exposes this
   plugin to the same race the other Ultimate-Multisite connector plugins
   hit: WP core's routes/connectors-home/content module runs
   registerDefaultConnectors() inside an async dynamic import, after our
   top-level registerConnector() has already populated the store, and the
   reducer's spread overwrites our custom render with the generic
   ApiKeyConnector. Re-assert the registration on five ticks (sync +
   microtask + setTimeout 0/50/250/1000ms) so we always end up last.

The proper upstream fix is in WordPress/gutenberg#77116. Once that ships
in a Gutenberg release, the 5-tick workaround can be removed; the rename
is permanent.

CSS class on the rendered card also updated from
`connector-item--anthropic-max` to `connector-item--ultimate-ai-connector-anthropic-max`
to match the new slug and the convention used by the sister plugins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
superdav42 added a commit to Ultimate-Multisite/ai-provider-for-anthropic-max that referenced this pull request Apr 8, 2026
…ix render clobber (#6)

* v1.0.1: defensive re-register on Connectors page

WP core's routes/connectors-home/content.js runs registerDefaultConnectors()
from inside an async dynamic import, and the connectors store reducer
spreads new config over existing entries — so the default's
`args.render = ApiKeyConnector` can overwrite a plugin's custom render.

This plugin currently dodges the bug because its JS slug
(ai-provider-for-anthropic-max/connector) doesn't collide with the PHP-side
provider id (anthropic-max). That's fragile — a future upstream change to
slug normalization or sanitization could break it without warning.

Re-assert the registration on five ticks (sync + microtask + setTimeout
0/50/250/1000ms) so we always end up last regardless of dynamic-import
resolution order. Idempotent and cheap.

The proper upstream fix is WordPress/gutenberg#77116. Once that ships in
a Gutenberg release, this defense can be removed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* v1.1.0: rename provider id to ultimate-ai-connector-anthropic-max + fix render clobber

This commit makes two coordinated changes that have to ship together:

1. RENAME the AI Client provider id from 'anthropic-max' to
   'ultimate-ai-connector-anthropic-max', and the JS registerConnector()
   slug from 'ai-provider-for-anthropic-max/connector' to the same
   'ultimate-ai-connector-anthropic-max'. This:

   - Matches the naming convention used by the sister plugins
     (ultimate-ai-connector-webllm, ultimate-ai-connector-compatible-endpoints).
   - Claims the 'ultimate-ai-connector-' namespace properly so a future
     third-party 'anthropic-max' plugin can't collide with this one.
   - Causes the WP Connectors page to render ONE card instead of two —
     previously a hidden duplicate auto-registered ApiKeyConnector card
     existed alongside the custom-rendered card because the slugs differed.

   This is a BREAKING change for any caller that hardcoded the old
   provider id (e.g. AiClient::defaultRegistry()->getProvider('anthropic-max')).
   Stored OAuth tokens, REST endpoint URLs, option keys, transient prefixes,
   plugin folder name, text domain, and css class are unchanged.

2. WORKAROUND for WP core's registerDefaultConnectors() clobbering custom
   renders. The rename in (1) makes the slugs match, which exposes this
   plugin to the same race the other Ultimate-Multisite connector plugins
   hit: WP core's routes/connectors-home/content module runs
   registerDefaultConnectors() inside an async dynamic import, after our
   top-level registerConnector() has already populated the store, and the
   reducer's spread overwrites our custom render with the generic
   ApiKeyConnector. Re-assert the registration on five ticks (sync +
   microtask + setTimeout 0/50/250/1000ms) so we always end up last.

The proper upstream fix is in WordPress/gutenberg#77116. Once that ships
in a Gutenberg release, the 5-tick workaround can be removed; the rename
is permanent.

CSS class on the rendered card also updated from
`connector-item--anthropic-max` to `connector-item--ultimate-ai-connector-anthropic-max`
to match the new slug and the convention used by the sister plugins.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
superdav42 added a commit to Ultimate-Multisite/ultimate-ai-connector-compatible-endpoints that referenced this pull request Apr 8, 2026
WP core's routes/connectors-home/content.js runs registerDefaultConnectors()
from inside an async dynamic import. By the time it executes, our top-level
registerConnector() has already run, and the connectors store reducer
spreads new config over the existing entry — so the default's
`args.render = ApiKeyConnector` overwrites our custom card. The user sees
the generic API-key UI instead of the endpoint URL / model picker form.

The proper upstream fix is WordPress/gutenberg#77116. Until that lands and
ships in a Gutenberg release, work around it by re-asserting our
registration on five ticks (sync + microtask + setTimeout 0/50/250/1000ms)
so we always end up last regardless of dynamic-import resolution order.

Re-registration with the same render is idempotent so the redundant calls
cost almost nothing.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@t-hamano t-hamano added [Type] Bug An existing feature does not function as intended Connectors screen Tracks connectors screen related tasks labels Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Connectors screen Tracks connectors screen related tasks First-time Contributor Pull request opened by a first-time contributor to Gutenberg repository [Type] Bug An existing feature does not function as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Connectors: registerDefaultConnectors() clobbers third-party custom render via async dynamic-import race

2 participants