From 499a649257c88b84641bf62da42c139f5d3c3d7a Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 20 Aug 2026 15:44:21 -0400 Subject: [PATCH] fix: normalize the trailing slash on an inserted site API namespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `apiPathModifierMiddleware` inserted `siteApiNamespace[0]` verbatim, so a namespace configured without a trailing slash ran into the following segment: `/wp/v2/sites/123` + `posts` produced `/wp/v2/sites/123posts`. Both forms are supported input — `WordPressRESTURL` and `RestUrlBuilder` normalize them identically for native-issued requests, with tests pinning the unslashed case. This middleware was the one consumer not applying that rule. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ULMqNwTEWty4MeNrr94MuF --- src/utils/api-fetch.js | 5 +++-- src/utils/api-fetch.test.js | 27 +++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/utils/api-fetch.js b/src/utils/api-fetch.js index 590a44780..d5fee136d 100644 --- a/src/utils/api-fetch.js +++ b/src/utils/api-fetch.js @@ -109,10 +109,11 @@ function apiPathModifierMiddleware( options, next ) { ).test( options.path ) || /\/sites\/[^/]+\//.test( options.path ); if ( isEligiblePath && ! alreadyHasSiteNamespace ) { - // Insert the API namespace after the first two path segments. + // Insert the API namespace after the first two path segments, with a + // single trailing slash. options.path = options.path.replace( /^(?\/?(?:[\w.-]+\/){2})/, - `$${ siteApiNamespace[ 0 ] }` + `$${ siteApiNamespace[ 0 ].replace( /\/+$/, '' ) }/` ); } diff --git a/src/utils/api-fetch.test.js b/src/utils/api-fetch.test.js index 63cac028b..f4025333a 100644 --- a/src/utils/api-fetch.test.js +++ b/src/utils/api-fetch.test.js @@ -212,6 +212,33 @@ describe( 'api-fetch credentials handling', () => { expect( requestedUrl() ).toContain( '/wp/v2/sites/123/posts' ); } ); + it( 'inserts a namespace configured without a trailing slash', async () => { + // Both forms are supported; the native URL builders normalize them + // identically. Without normalizing here the namespace would run into + // the following segment: `/wp/v2/sites/123posts`. + bridge.getGBKit.mockReturnValue( { + siteApiRoot: 'https://example.com/wp-json/', + siteApiNamespace: [ 'sites/123' ], + namespaceExcludedPaths: [], + } ); + + await apiFetch( { path: '/wp/v2/posts' } ).catch( () => {} ); + + expect( requestedUrl() ).toContain( '/wp/v2/sites/123/posts' ); + } ); + + it( 'does not double the slash on a namespace that already ends with one', async () => { + bridge.getGBKit.mockReturnValue( { + siteApiRoot: 'https://example.com/wp-json/', + siteApiNamespace: [ 'sites/123/' ], + namespaceExcludedPaths: [], + } ); + + await apiFetch( { path: '/wp/v2/posts' } ).catch( () => {} ); + + expect( requestedUrl() ).not.toContain( 'sites/123//' ); + } ); + it( 'leaves the path alone when it already carries the namespace', async () => { bridge.getGBKit.mockReturnValue( { siteApiRoot: 'https://example.com/wp-json/',