Skip to content

Three Ways to Solve CORS Issue in the Embed Swagger Page in Backstage

Embed-swagger-Cors-cover.jpg

As we mentioned in this article before, centralizing all the needed knowledge in one developer portal is a big improvement in daily working experience and convenience.

But we face a CORS problem when sending requests by an embedded swagger page in the API definition. this problem will significantly reduce the functionality of the Swagger page, so this article proposes three ways to solve it:

  1. allow the App to cross-origin for your Backstage domain
  2. provide modified plain-text open API JSON and add proxy
  3. change the server URL when rendering pages and add proxy

Let's go through them in detail!


Problem reproduce (As-Is)

After our basic setup in previous article, when sending a request from the Swagger page will look like this:

as-is-API.png

here we can discover two problems: 1. the current URL is localhost:3000, while the target server URL is localhost:8081 2. the first problem led to the server response with a CORS error ai-is-network.png

So by default, we can not send requests through the embedded Swagger page in the Backstage.


Solution (To-Be)

Here I propose three lightweight (little code modified) solutions, we can choose one of them according to the security concern, and existing CI/CD process.

1. Allow CORS in The APP

The easiest way is to allow the App to cross-origin for your Backstage domain, if it's OK to modify your app setting, which might have these side effects: 1. your app in test env (and add logic to disable it in prod env). 2. left some code in your codebase (take my spring boot application for example).

OrderController.java
@RestController
// to allow only Backstage (domain= http://localhost:3000) to send request
@CrossOrigin(origins = "http://localhost:3000") 
...
public class OrderController {...}
In this way, the swagger page can successfully send requests directly to the app.

2. provide modified plain-text JSON, and add proxy

Needed Modification

If your open API spec is provided by a static file generated in a CI/CD process, then it is a good way to add a customized step to modify the original URL to Backstage's proxy endpoint, and the Backstage backend will proxy the request and send to your app without a CORS error.

to enable the proxy setting, we have to rewrite/ add the servers.url string with a specific key (i.e. /order-command in my example).

api-docs.json
{
  "openapi": "3.0.1",
  "info": {
    "title": "OpenAPI definition",
    "version": "v0"
  },
  "servers": [
    { // origin URL is http://localhost:8081
      "url": "http://localhost:7007/api/proxy/order-command",
      "description": "Generated server URL"
    }
  ],
  ...
}

and add the following setting in the app-config.yaml in the Backstage project

app-config.yaml
proxy:
  '/order-command':
    target: 'http://localhost:8081'
    changeOrigin: true
    pathRewrite: 
      '^/api/proxy/order-command': '/'

Result

after starting the Backstage, we can first see these logs show the Proxy is created.

[1] 2023-10-22T09:14:37.849Z proxy info [HPM] Proxy created: /order-command  -> http://localhost:8081 type=plugin
[1] 2023-10-22T09:14:37.849Z proxy info [HPM] Proxy rewrite rule created: "^/api/proxy/order-command" ~> "/" type=plugin
then the URL on Swagger will change to the proxy endpoint of the Backstage backend.

to-be_2_servers_url.png

In this case, the request will successfully be sent to the Backstage backend and be proxy to the correct App endpoint and respond normally.

to-be_2_request.png

3. change the server URL when render page and add proxy

If you have concerns about allowing CORS on the App and don't have an existing CI/CD process to generate a static open API file. Then we should use a customized API entity renderer to do the URL modification task in real time.

Needed Modification

Refer to this Custom API Renderings tutorial. we can first add @types/swagger-ui-react and swagger-ui-react to the package/app, then change the packages/app/src/apis.ts to a .tsx file and add the following (see the diff in my commit):

packages/app/src/apis.tsx
import {ApiEntity } from '@backstage/catalog-model';
import {
apiDocsConfigRef,
defaultDefinitionWidgets
} from '@backstage/plugin-api-docs';

export const apis: AnyApiFactory[] = [
  createApiFactory({
    ...
  }),
  ScmAuth.createDefaultApiFactory(),
  // add the below code snippet
  createApiFactory({
      api: apiDocsConfigRef,
      deps: {},
      factory: () => {
        // load the default widgets
        const definitionWidgets = defaultDefinitionWidgets();
        return {
          getApiDefinitionWidget: (apiEntity: ApiEntity) => {
            // custom rendering for solve cors issue
            if (apiEntity.spec.type === 'cors-openapi') {
              let regex = /"servers":\[{"url":"([a-z]+:\/\/[a-zA-Z-.:0-9]+)"/g;
              let matches = regex.exec(apiEntity.spec.definition);
              let targetString = matches ? matches[1] : "";

              apiEntity.spec.definition = apiEntity.spec.definition.replaceAll(
               regex,
               "\"servers\":[{\"url\":\"http://localhost:7007/api/proxy/" + targetString + "\"");

               apiEntity.spec.type='openapi';
            }
            // fallback to the defaults
            return definitionWidgets.find(d => d.type === apiEntity.spec.type);
          },
        };
      },
    }),
    // add the above code snippet
];

finally, add the corresponding proxy setting in app-config.yaml like:

app-config.yaml
proxy:
  '/http://localhost:8081':
    target: 'http://localhost:8081'
    changeOrigin: true
    pathRewrite:
      '^/api/proxy/http://localhost:8081': 'http://localhost:8081'

and the only thing we need to modify the spec.type to cors-openapi in our API definition yaml.

order-command-side-api.yaml
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
  ...
spec:
  type: cors-openapi
  lifecycle: experimental
  owner: guests
  definition:
    $text: http://localhost:8081/v3/api-docs

Result

then the URL on Swagger will change to the proxy endpoint of the Backstage backend with the original URL.

to-be_3_servers_url.png

In this case, the request will also successfully be sent to the Backstage backend and be proxy to the correct App endpoint and respond normally.

to-be_3_request.png


Summary

Using three ways proposed in this article can solve the CORS issue with slight changes in the project or the Backstage app. This will bring more convenience when others integrate/try our API by reading on the Backstage.

The developer portal is a very powerful tool to improve developers' experience, but it also needs some effort to build some guidelines, plugins, or mechanisms on the portal App (i.e. Backstage), which can be done by a platform engineering team or task force. After the hard work, you will find it very worthy to have a well-done developer portal.

reference