Skip to content

fix(openapi): correctly merge plugin router prefix with route paths#25616

Merged
jhoward1994 merged 5 commits intostrapi:developfrom
Akash504-ai:fix/openapi-plugin-prefix
Mar 9, 2026
Merged

fix(openapi): correctly merge plugin router prefix with route paths#25616
jhoward1994 merged 5 commits intostrapi:developfrom
Akash504-ai:fix/openapi-plugin-prefix

Conversation

@Akash504-ai
Copy link
Copy Markdown
Contributor

@Akash504-ai Akash504-ai commented Mar 3, 2026

What does it do?

Fixes an issue in the OpenAPI generator where plugin route prefixes were not merged with their route paths.

PluginRoutesProvider was returning router.routes directly and ignoring router.prefix, causing prefixed plugin routes (e.g. upload) to be generated under / instead of their actual base path.

This change merges router.prefix with route.path and normalizes the resulting path to prevent duplicate or trailing slashes.


Why is it needed?

When running: strapi openapi generate

Upload plugin routes were generated like:

"/": {
  "operationId": "upload/post"
}

Instead of:

"/upload": {
  "operationId": "upload/post"
}

This resulted in incorrect OpenAPI specifications for plugin routes.

How to test it?

  1. Checkout develop
  2. Run yarn build
  3. Navigate to examples/kitchensink-ts
  4. Run yarn develop
  5. Run yarn strapi openapi generate
  6. Open specification.json

Before fix:

"/": { "operationId": "upload/post" }

After fix:

"/upload": { "operationId": "upload/post/upload" }
"/upload/files"
"/upload/files/:id"

Related issue(s)/PR(s)

Fixes #25591

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 3, 2026

@Akash504-ai is attempting to deploy a commit to the Strapi Team on Vercel.

A member of the Team first needs to authorize it.

@github-actions github-actions Bot added the community Changes and fixes created by community members label Mar 3, 2026
@dosubot dosubot Bot added pr: fix This PR is fixing a bug source: core:openapi Source is core/openapi package labels Mar 3, 2026
@nclsndr nclsndr self-assigned this Mar 4, 2026
@nclsndr
Copy link
Copy Markdown
Contributor

nclsndr commented Mar 4, 2026

Thank your for raising this PR @Akash504-ai!
I'm making a pre-check on the feature design with the team. I'll then get back here for the code review.

@nclsndr nclsndr added this to the 5.38.1 milestone Mar 4, 2026
: // Else, extract and flatten every route from each router
Object.values(routes).flatMap((router) => router.routes);
? routes
: Object.values(routes).flatMap((router: any) => {
Copy link
Copy Markdown
Contributor

@jhoward1994 jhoward1994 Mar 9, 2026

Choose a reason for hiding this comment

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

Could we use the type "router: Core.Router" here?

(import type { Core } from '@strapi/types';)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll update the type to router: Core.Router instead of any and push the change.

Object.values(routes).flatMap((router) => router.routes);
? routes
: Object.values(routes).flatMap((router: any) => {
const prefix = router.prefix ?? '';
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.

I think this might have to be something like this

  const hasOwnPrefix =
    route.config != null &&
    Object.prototype.hasOwnProperty.call(route.config, 'prefix');
  const effectivePrefix = hasOwnPrefix
    ? (route.config.prefix ?? '')
    : (router.prefix ?? '');

Otherwise the changes would generate incorrect OpenAPI paths for e.g. packages/plugins/users-permissions/server/routes/content-api/auth.js as they have route level prefixes defined at config.prefix ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll update the implementation to respect route-level prefixes before falling back to router.prefix and push the change shortly.

@jhoward1994 jhoward1994 self-assigned this Mar 9, 2026
@Akash504-ai
Copy link
Copy Markdown
Contributor Author

Hi! @jhoward1994 I have pushed the requested changes.
The router type now uses Core.Router and the implementation respects route-level config.prefix before falling back to router.prefix. I also verified the build locally. Please let me know if any further adjustments are needed.

@jhoward1994
Copy link
Copy Markdown
Contributor

Thanks @Akash504-ai !

One more thing, would you be able to add this code to packages/core/openapi/__tests__/routes/plugins-route-provider.test.ts starting at L20

This will make sure we have unit tests that cover this new behaviour

    it('should prepend router prefix to route paths', () => {
      const strapiMock = {
        plugins: {
          upload: {
            routes: {
              'content-api': {
                type: 'content-api',
                prefix: '/upload',
                routes: [
                  {
                    method: 'GET',
                    path: '/',
                    handler: 'controller.find',
                    info: { type: 'content-api' },
                  },
                  {
                    method: 'GET',
                    path: '/files/:id',
                    handler: 'controller.findOne',
                    info: { type: 'content-api' },
                  },
                ],
              },
            },
          },
        },
      } as unknown as Core.Strapi;

      const provider = new PluginRoutesProvider(strapiMock);
      const { routes } = provider;

      expect(routes).toHaveLength(2);
      expect(routes[0].path).toBe('/upload');
      expect(routes[1].path).toBe('/upload/files/:id');
    });

    it('should use route config.prefix instead of router prefix when present', () => {
      const strapiMock = {
        plugins: {
          'users-permissions': {
            routes: {
              'content-api': {
                type: 'content-api',
                prefix: '/users-permissions',
                routes: [
                  {
                    method: 'POST',
                    path: '/auth/local',
                    handler: 'auth.callback',
                    info: { type: 'content-api' },
                    config: { prefix: '' },
                  },
                  {
                    method: 'GET',
                    path: '/users/me',
                    handler: 'user.me',
                    info: { type: 'content-api' },
                    config: { prefix: '' },
                  },
                ],
              },
            },
          },
        },
      } as unknown as Core.Strapi;

      const provider = new PluginRoutesProvider(strapiMock);
      const { routes } = provider;

      expect(routes).toHaveLength(2);
      // These routes have config.prefix = '', so they bypass the router prefix
      expect(routes[0].path).toBe('/auth/local');
      expect(routes[1].path).toBe('/users/me');
    });

    it('should handle mix of routes with and without config.prefix', () => {
      const strapiMock = {
        plugins: {
          'users-permissions': {
            routes: {
              'content-api': {
                type: 'content-api',
                prefix: '/users-permissions',
                routes: [
                  {
                    method: 'POST',
                    path: '/auth/local',
                    handler: 'auth.callback',
                    info: { type: 'content-api' },
                    config: { prefix: '' },
                  },
                  {
                    method: 'GET',
                    path: '/roles',
                    handler: 'role.find',
                    info: { type: 'content-api' },
                  },
                ],
              },
            },
          },
        },
      } as unknown as Core.Strapi;

      const provider = new PluginRoutesProvider(strapiMock);
      const { routes } = provider;

      expect(routes).toHaveLength(2);
      // config.prefix = '' bypasses router prefix
      expect(routes[0].path).toBe('/auth/local');
      // No config.prefix, uses router prefix
      expect(routes[1].path).toBe('/users-permissions/roles');
    });

    it('should handle routes with no router prefix', () => {
      const strapiMock = {
        plugins: {
          test: {
            routes: {
              'content-api': {
                type: 'content-api',
                routes: [
                  {
                    method: 'GET',
                    path: '/items',
                    handler: 'controller.find',
                    info: { type: 'content-api' },
                  },
                ],
              },
            },
          },
        },
      } as unknown as Core.Strapi;

      const provider = new PluginRoutesProvider(strapiMock);
      const { routes } = provider;

      expect(routes).toHaveLength(1);
      expect(routes[0].path).toBe('/items');
    });

@Akash504-ai
Copy link
Copy Markdown
Contributor Author

Akash504-ai commented Mar 9, 2026

@jhoward1994 I have added the unit tests for the plugin route prefix behavior and route-level prefix overrides. I also ran the tests locally to ensure everything passes. Please let me know if any adjustments are needed.

@nclsndr
Copy link
Copy Markdown
Contributor

nclsndr commented Mar 9, 2026

thank your for your help @Akash504-ai!

@Akash504-ai
Copy link
Copy Markdown
Contributor Author

You're welcome @nclsndr! Happy to help

@jhoward1994 jhoward1994 merged commit ff8912a into strapi:develop Mar 9, 2026
94 of 95 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Changes and fixes created by community members pr: fix This PR is fixing a bug source: core:openapi Source is core/openapi package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openapi generator messes up upload paths.

3 participants