openapistack / openapi-backend

Build, Validate, Route, Authenticate and Mock using OpenAPI
https://openapistack.co
MIT License
622 stars 83 forks source link

Is there a way to serve multiple mocks under same path ? #92

Open snakuzzo opened 4 years ago

snakuzzo commented 4 years ago

Hi, Maybe it's not possible, but I'm trying to serve multiple mocks under same path but without success. I have multiple openapi spec files and I need to create a unique mock server The only way I found is to use different routes like this:

app.use('/' + spec + '/', (req, res) => api.handleRequest(req, req, res));

where spec is the name of every single spec. And the result is:

/spec1/api/pets
/spec2/api/books
/spec3/another/service
...

Is there a way to serve all under same path (eg. / ) ?

Thank you

anttiviljami commented 4 years ago

Hi @snakuzzo! Sure it's possible. You just need a little bit of logic. Something like (this code is untested but hopefully gives you an idea

app.use('/', async (req, res) => {
  const [res1, res2, res3] = await Promise.all([
    api1.handleRequest(req, req, res),
    api2.handleRequest(req, req, res),
    api3.handleRequest(req, req, res),
  ]);
  if (res1.statusCode !== 404) return res1;
  if (res2.statusCode !== 404) return res2;
  if (res3.statusCode !== 404) return res3;
  return res.status(404).end();
});
snakuzzo commented 4 years ago

I tried, but it doesn't work. I've got an array of OpenAPIBackend, so this is my code...

      app.use('/', async (req, res) => {
        const [res1, res2] = await Promise.all([
          apiList[0].handleRequest(req, req, res),
          apiList[1].handleRequest(req, req, res)
        ]);
        if (res1.statusCode !== 404) return res1;
        if (res2.statusCode !== 404) return res2;
        return res.status(404).end();
      });

but when I try it raises an UnhandledPromiseRejectionWarning

UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

snakuzzo commented 4 years ago

One handleRequest OK...

       app.use('/', async (req, res) => {
        const [res1, res2] = await Promise.all([
          apiList[0].handleRequest(req, req, res)
        ]);
        if (res1.statusCode !== 404) return res1;
        return res.status(404).end();
      });

::1 - - [17/Jun/2020:12:54:27 +0000] "POST /api/myendpoint/111111 HTTP/1.1" 200 217 "-" "PostmanRuntime/7.25.0"

More than one handleRequest KO...

       app.use('/', async (req, res) => {
        const [res1, res2] = await Promise.all([
          apiList[0].handleRequest(req, req, res),
          apiList[1].handleRequest(req, req, res)
        ]);
        if (res1.statusCode !== 404) return res1;
        if (res2.statusCode !== 404) return res2;
        return res.status(404).end();
      });
::1 - - [17/Jun/2020:12:56:23 +0000] "POST /api/myendpoint/111111 HTTP/1.1" 404 19 "-" "PostmanRuntime/7.25.0"
(node:31034) UnhandledPromiseRejectionWarning: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at ServerResponse.setHeader (_http_outgoing.js:535:11)
    at ServerResponse.header (/home/snakuzzo/workspace/mockserver/node_modules/express/lib/response.js:771:10)
    at ServerResponse.send (/home/snakuzzo/workspace/mockserver/node_modules/express/lib/response.js:170:12)
    at ServerResponse.json (/home/snakuzzo/workspace/mockserver/node_modules/express/lib/response.js:267:15)
    at notImplemented (/home/snakuzzo/workspace/mockserver/app.js:98:45)
    at OpenAPIBackend.<anonymous> (/home/snakuzzo/workspace/mockserver/node_modules/openapi-backend/backend.js:260:24)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at async OpenAPIBackend.handleRequest (/home/snakuzzo/workspace/mockserver/node_modules/openapi-backend/backend.js:161:26)
    at async Promise.all (index 0)
    at async /home/snakuzzo/workspace/mockserver/app.js:128:30
(node:31034) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:31034) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
snakuzzo commented 4 years ago

Tried...

       app.use('/', async (req, res) => {
         try {
          let [res1,res2] = await Promise.all([
            apiList[0].handleRequest(req, req, res),
            apiList[1].handleRequest(req, req, res)
          ]);
          if (res.statusCode !== 404) return res
          return res.status(404).end();
         } catch(error) {
          console.log(error)
         }
      });

error...

::1 - - [17/Jun/2020:15:31:03 +0000] "POST /api/myendpoint/111111 HTTP/1.1" 404 19 "-" "PostmanRuntime/7.25.0"
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
    at ServerResponse.setHeader (_http_outgoing.js:535:11)
    at ServerResponse.header (/home/snakuzzo/workspace/mockserver/node_modules/express/lib/response.js:771:10)
    at ServerResponse.send (/home/snakuzzo/workspace/mockserver/node_modules/express/lib/response.js:170:12)
    at ServerResponse.json (/home/snakuzzo/workspace/mockserver/node_modules/express/lib/response.js:267:15)
    at notImplemented (/home/snakuzzo/workspace/mockserver/app.js:98:45)
    at OpenAPIBackend.<anonymous> (/home/snakuzzo/workspace/mockserver/node_modules/openapi-backend/backend.js:260:24)
    at processTicksAndRejections (internal/process/task_queues.js:97:5)
    at async OpenAPIBackend.handleRequest (/home/snakuzzo/workspace/mockserver/node_modules/openapi-backend/backend.js:161:26)
    at async Promise.all (index 0)
    at async /home/snakuzzo/workspace/mockserver/app.js:131:29 {
  code: 'ERR_HTTP_HEADERS_SENT'
snakuzzo commented 4 years ago

I'm stuck... :(

Here a full example to reproduce the error... any solution?


const OpenAPIBackend = require('openapi-backend').default;
const express = require('express');
const app = express();
app.use(express.json());

// define api
const api1 = new OpenAPIBackend({
  definition: {
    openapi: '3.0.1',
    info: {
      title: 'My API',
      version: '1.0.0',
    },
    paths: {
      '/pets': {
        get: {
          operationId: 'getPets',
          responses: {
            200: { description: 'ok' },
          },
        },
      },
      '/pets/{id}': {
        get: {
          operationId: 'getPetById',
          responses: {
            200: { description: 'ok' },
          },
        },
        parameters: [
          {
            name: 'id',
            in: 'path',
            required: true,
            schema: {
              type: 'integer',
            },
          },
        ],
      },
    },
  },
  handlers: {
    getPets: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
    getPetById: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
    validationFail: async (c, req, res) => res.status(400).json({ err: c.validation.errors }),
    notFound: async (c, req, res) => res.status(404).json({ err: 'not found' }),
    notImplemented: async (c, req, res) => {
      const { status, mock } = c.api.mockResponseForOperation(c.operation.operationId);
      return res.status(status).json(mock);
    },
  },
});

const api2 = new OpenAPIBackend({
  definition: {
    openapi: '3.0.1',
    info: {
      title: 'My Other API',
      version: '1.0.0',
    },
    paths: {
      '/pets1': {
        get: {
          operationId: 'getPets',
          responses: {
            200: { description: 'ok' },
          },
        },
      },
      '/pets1/{id}': {
        get: {
          operationId: 'getPetById',
          responses: {
            200: { description: 'ok' },
          },
        },
        parameters: [
          {
            name: 'id',
            in: 'path',
            required: true,
            schema: {
              type: 'integer',
            },
          },
        ],
      },
    },
  },
  handlers: {
    getPets: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
    getPetById: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
    validationFail: async (c, req, res) => res.status(400).json({ err: c.validation.errors }),
    notFound: async (c, req, res) => res.status(404).json({ err: 'not found' }),
    notImplemented: async (c, req, res) => {
      const { status, mock } = c.api.mockResponseForOperation(c.operation.operationId);
      return res.status(status).json(mock);
    },
  },
});

api1.init();
api2.init();

// use as express middleware
//app.use((req, res) => api.handleRequest(req, req, res));

app.use('/', async (req, res) => {
  try {
   let [res1,res2] = await Promise.all([
     api1.handleRequest(req, req, res),
     api2.handleRequest(req, req, res)
   ]);
   if (res1.statusCode !== 404) return res1.end();
   if (res2.statusCode !== 404) return res2.end();
   return res.status(404).end();
  } catch(error) {
   console.log(error)
  }
});

// start server
app.listen(8080, () => console.info('api listening at http://localhost:8080'));
Galiley commented 3 years ago

@snakuzzo You can solve this with something like

const OpenAPIBackend = require('openapi-backend').default;
const express = require('express');
const { STATUS_CODES } = require("http");
const app = express();
app.use(express.json());

// define api
const api1 = new OpenAPIBackend({
  definition: {
    openapi: '3.0.1',
    info: {
      title: 'My API',
      version: '1.0.0',
    },
    paths: {
      '/pets': {
        get: {
          operationId: 'getPets',
          responses: {
            200: { description: 'ok' },
          },
        },
      },
      '/pets/{id}': {
        get: {
          operationId: 'getPetById',
          responses: {
            200: { description: 'ok' },
          },
        },
        parameters: [
          {
            name: 'id',
            in: 'path',
            required: true,
            schema: {
              type: 'integer',
            },
          },
        ],
      },
    },
  },
  handlers: {
    getPets: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
    getPetById: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
    validationFail: async (c, req, res) => res.status(400).json({ err: c.validation.errors }),
    notFound: async (c, req, res) => res.status(404).json({ err: 'not found' }),
    notImplemented: async (c, req, res) => {
      const { status, mock } = c.api.mockResponseForOperation(c.operation.operationId);
      return res.status(status).json(mock);
    },
  },
});

const api2 = new OpenAPIBackend({
  definition: {
    openapi: '3.0.1',
    info: {
      title: 'My Other API',
      version: '1.0.0',
    },
    paths: {
      '/pets1': {
        get: {
          operationId: 'getPets',
          responses: {
            200: { description: 'ok' },
          },
        },
      },
      '/pets1/{id}': {
        get: {
          operationId: 'getPetById',
          responses: {
            200: { description: 'ok' },
          },
        },
        parameters: [
          {
            name: 'id',
            in: 'path',
            required: true,
            schema: {
              type: 'integer',
            },
          },
        ],
      },
    },
  },
  handlers: {
    getPets: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
    getPetById: async (c, req, res) => res.status(200).json({ operationId: c.operation.operationId }),
    validationFail: async (c, req, res) => res.status(400).json({ err: c.validation.errors }),
    notFound: async (c, req, res) => res.status(404).json({ err: 'not found' }),
    notImplemented: async (c, req, res) => {
      const { status, mock } = c.api.mockResponseForOperation(c.operation.operationId);
      return res.status(status).json(mock);
    },
  },
});

api1.init();
api2.init();

// use as express middleware
//app.use((req, res) => api.handleRequest(req, req, res));

app.use(async (req, res, next) => {
    try {
      const match = [api1, api2].filter(
        (v) => v.matchOperation(req, false) !== undefined
      );
      switch (match.length) {
        case 0:
          //Not found in any api
          next();
          break;
        case 1:
          match[0].handleRequest(req, req, res).catch(next);
          break;
        default:
          next({
            status: 500,
            message: `Possible operationId collision (${match
              .map((v) => v.definition.info.title)
              .join(", ")})`,
          });
      }
    } catch (error) {
      next(error);
    }
 });
 app.use("*", (req, res) => {
    res.status(404).json({
      message: `${req.baseUrl || req.originalUrl} not found`,
      code: STATUS_CODE[404],
    });
});

app.use((err, req, res, next) => {
    const status = err.status || 500;
    if (status >= 500) {
      console.error(err);
    }
    res.status(status).json({
      message: err.message,
      errors: err.errors,
      code: STATUS_CODES[status] || "Internal server error",
    });
  });

// start server
app.listen(8080, () => console.info('api listening at http://localhost:8080'));