apigee-127 / swagger-tools

A Node.js and browser module that provides tooling around Swagger.
MIT License
701 stars 373 forks source link

Different url in swaggerUi and swaggerRouter when using them as sub-app #565

Closed andrzejzysko closed 6 years ago

andrzejzysko commented 6 years ago

Problem

How to change url api path for swaggerUi where using express sub-app.

The goal is to build one app serving others API as sub-app. The sub-apps have common pattern and are served in root app.

sub-app/index.js

'use strict';

var app = require('express')();
var http = require('http');
var swaggerTools = require('swagger-tools');
var jsyaml = require('js-yaml');
var fs = require('fs');
var serverPort = 8081;

// swaggerRouter configuration
var options = {
  swaggerUi: __dirname + '/swagger.json',
  controllers: __dirname + '/controllers',
  useStubs: process.env.NODE_ENV === 'development' ? true : false // Conditionally turn on stubs (mock mode)
};

// The Swagger document (require it, build it programmatically, fetch it from a URL, ...)
var spec = fs.readFileSync(__dirname + '/api/swagger.yaml', 'utf8');
var swaggerDoc = jsyaml.safeLoad(spec);
swaggerDoc.host = 'localhost:' + serverPort;

// Initialize the Swagger middleware
swaggerTools.initializeMiddleware(swaggerDoc, function (middleware) {
  // Interpret Swagger resources and attach metadata to request - must be first in swagger-tools middleware chain
  app.use(middleware.swaggerMetadata());

  // Validate Swagger requests
  app.use(middleware.swaggerValidator());

  // Route validated requests to appropriate controller
  app.use(middleware.swaggerRouter(options));

  // Serve the Swagger documents and Swagger UI
  app.use(middleware.swaggerUi());

  // Start the server
  // http.createServer(app).listen(serverPort, function () {
  //   console.log('Your server is listening on port %d (http://localhost:%d)', serverPort, serverPort);
  //   console.log('Swagger-ui is available on http://localhost:%d/docs', serverPort);
  // });
});

module.exports = app;

swagger.yaml part

---
swagger: "2.0"
info:
  description: ""
  version: "v3"
  title: "Customer Management"
host: "localhost"
basePath: "/v3/customerManagement"
schemes:
- "http"
consumes:
- "application/json"
produces:
- "application/json"
paths:
  /customer:
    get:
(...)

root-app

const express = require('express');
const app = express();
const serverPort = 8081;

const customerManagement = require(__dirname + '/sub-app/index.js');
app.use(['/customerManagement'], customerManagement);

Problem details

The result is that swaggerUi is served under fallowing url:

http://localhost:8081/customerManagement/docs/

API is served under fallowing url:

http://localhost:8081/customerManagement/v3/customerManagement/customer

But when you are trying to use swaggerUi to test this API then the url is:

http://localhost:8081/v3/customerManagement/customer

How to change url path in swaggerUi?

whitlockjc commented 6 years ago

swagger-ui uses the details from the spec, specifically basePath and host. Are you sure this is a swagger-tools issue?

andrzejzysko commented 6 years ago

Yes, I know that it comes from the spec file. (To be clear and precise host and basePathare on the top level not in the info object). The problem is that I'm using Node.js express sub-app functionality. It means that finally host part of the url for the API will be different then oryginal spec but swagger-ui will still use the same url from file. In my example host, basePath, schemes and path are:

host: "localhost"
basePath: "/v3/customerManagement"
schemes:
- "http"
paths:
  /customer:

So the API will be exposed under this url: http://localhost/v3/customerManagement/customer but when you use sub-app functionality the API will be exposed under this url: http://localhost/customerManagement/v3/customerManagement/customer but swagger-ui still will use oryginal specification: http://localhost/v3/customerManagement/customer. The question is: How to set different url for swagger-ui or swagger-router?

whitlockjc commented 6 years ago

If you are telling express to use a mountpoint or some other feature that prefixes the actual API paths, you need to make sure your basePath in the Swagger document corresponds. That's the only way I know how to but I'm open to ideas.

andrzejzysko commented 6 years ago

If I change basePath adding the same prefix as mountpoint in express then this prefix will be added 2 times for API (1 from mountpoint 1 from basePath) and for swagger-ui only 1 (only from basePath). I was thinking about adding this prefix to host instead of basePath but the swagger file specification wan't be correct.

whitlockjc commented 6 years ago

I'm not sure I understand what "added two times for API" means. Does swagger-tools miss matching the API requests or is swagger-ui doing something wrong, or both?

andrzejzysko commented 6 years ago

Let's consider simple examples: I'll put fallowing url to swagger file specification using schema, host and basePath: http://localhost:8080/basePath/resourcePart. Examples:

  1. Use prefix /foo' inexpress`.

    • API url will be: http://localhost:8080/foo/basePath/resourcePart
    • swagger-ui will use: http://localhost:8080/basePath/resourcePart
  2. Use prefix /foo/bar in express.

    • API url will be: http://localhost:8080/foo/bar/basePath/resourcePart
    • swagger-ui will use: http://localhost:8080/basePath/resourcePart

I would like to do something like this:

(...)
app.use(swaggerMetadata(swaggerDoc));
app.use(swaggerValidator());
app.use(swaggerRouter({
  controllers: './controllers'
});
swaggerDoc.basePath = '/samePrefixAsInExpress' + swaggerDoc.basePath; 
app.use(swaggerUi(swaggerDoc));
(...)
farrago commented 6 years ago

@andrzejzysko I have found and resolved a similar issue myself.

Internal and External paths

The problem is that you have internal and external views of the paths. As far as swagger-tools is concerned, it doesn't know it is actually being served under /foo/bar. The swagger-router just uses the paths from the swagger yaml as-is; i.e. handling an internal path of /basePath/resourcePart. But because of the url prefix, the true external path to request that path has /foo/bar pre-pended to it giving a full path of /foo/bar/basePath/resourcePath as you state.

Critically, the swaggerUI is created directly from the same yaml file (by default), and still without knowledge of the path prefix of /foo/bar. So it also shows the exact paths that are in the yaml, but these are the internal paths, not the full external paths as needed for the browser to call them.

Equally, you can't just change the paths in the yaml file to be the full external paths to make the swaggerUI correct, as that would make the router wrong (and you end up with it only responding to an external path of /foo/bar/foo/bar/basePath/...).

Solution

The solution I came up with was:

  1. Define the basePath in the swagger yaml as the full external base path: /foo/bar/basePath.
    • This allows the file to be shared with external groups correctly, and needs no knowledge of the actual implementation to build clients.
  2. After the yaml is loaded, make a clone of the document object (_.cloneDeep(), or JSON.parse(JSON.stringify(...)), or just load the file again etc.)
    • This gives us 2 independent copies of the swagger definition object.
    • It must be a clone because we are going to change the basePath of one of them, and if it's just another reference to the same object then both references will point to the same modified object.
  3. Modify the basePath in the cloned document to remove the prefix used by your mount point e.g remove /foo/bar.
    • This converts the external paths in your document, to the internal paths required by the router under the mount point.
  4. Pass the cloned & modified swagger document as the first parameter to initializeMiddleware(clonedAndModifiedSwaggerDoc, ...)
    • This initializes the middleware (and router) with the version with the internal paths
  5. Pass the unmodified, original swagger document to the swagger-ui middleware as the first parameter: swaggerUi(originalSwaggerDoc, ...)
    • This initializes the swagger-ui using the external paths so that the browser calls the correct path.
    • Although the swaggerUI docs don't mention it, you can see that first parameter to this middleware is indeed the swagger document object by looking at the code.

This gives you a working swagger-router using the internal paths needed because they are running behind a mount point, and a working swagger-ui using the external paths needed so that the browser can call the full path.

Example code

We use JSON file format for the swagger doc and an Express Router for the sub-application, but the general code is essentially (from memory, so may not be exact!):

const router = expresss.Router();

// Load swagger doc (file defined with full external paths)
const originalSwaggerDocWithExternalPaths = require('path/to/swagger.json');

// Clone the doc to allow us to make a version with _internal_ paths for most of the middleware
const clonedSwaggerDocWithInternalPaths = _.cloneDeep(originalSwaggerDoc);

// Remove the the first 8 chars from `basePath` to remove "/foo/bar"
clonedSwaggerDocWithInternalPaths.basePath = basePath.slice(8);

// Initialize swaggerTools middleware with the _internal_ paths version
swaggerTools.initializeMiddleware(clonedSwaggerDocWithInternalPaths, , function (middleware) {
  router.use(middleware.swaggerMetadata());

  // ...etc...

  // Serve the Swagger documents and Swagger UI using the _external_ paths version
  router.use(middleware.swaggerUi(originalSwaggerDocWithExternalPaths));
}

// Mount the router under "/foo/bar" to run as a sub-application
app.use('/foo/bar', router);

Hope this helps!

whitlockjc commented 6 years ago

So it seems like the middleware will work as expected but the URI in swagger-ui is wrong?

andrzejzysko commented 6 years ago

@whitlockjc maybe not wrong but static - can not be change independently to middleware where middleware register url based on mounting point. We have to think about this in swagger v3 because there is only one field servers instead of host, basePath and schemes.

OpenAPI 3.0 - API Server and Base URL In OpenAPI 3.0, you use the servers array to specify one or more base URLs for your API. servers replaces the host, basePath and schemes keywords used in OpenAPI 2.0.

@farrago thanks for sharing your approach. I was thinking exactly about the same workaround. The 2nd option is to mount sub-apps in the root / path and make changes in the basePath. If there is more sub-apps we have to change options for /api-docs and /docs to have them unique.

const swaggerUiOptions = {
  apiDocs: '/api-docs' + swaggerDoc.basePath,
  swaggerUi: '/docs' + swaggerDoc.basePath
};
app.use(middleware.swaggerUi(swaggerUiOptions));

I'm closing this issue as workaround is provided.