Skip to content

Commit

Permalink
feat: add support aborting requests in tags interface
Browse files Browse the repository at this point in the history
- support was added to HTTP Client for OAS operations
- support was added to Tags Interface

Older versions of swagger-client can use request
interceptors to inject abort signal into requests.

Refs #2349
  • Loading branch information
char0n committed Dec 21, 2021
1 parent da25c95 commit 7b6bdc2
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 0 deletions.
83 changes: 83 additions & 0 deletions docs/usage/http-client-for-oas-operations.md
Expand Up @@ -26,6 +26,7 @@ Property | Description
`attachContentTypeForEmptyPayload` | `Boolean=false`. Attaches a `Content-Type` header to a `Request` even when no payload was provided for the `Request`.
`http` | `Function=Http`. A function with an interface compatible with [HTTP Client](http-client.md).
`userFetch` | `Function=cross-fetch`. Custom **asynchronous** fetch function that accepts two arguments: the `url` and the `Request` object and must return a [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) object. More info in [HTTP Client](http-client.md) documentation.
`signal` | `AbortSignal=null`. AbortSignal object instance, which can be used to abort a request as desired.

For all later references, we will always use following OpenAPI 3.0.0 definition when referring
to a `spec`.
Expand Down Expand Up @@ -153,6 +154,88 @@ SwaggerClient.execute({
}); // => Promise.<Response>
```

#### Request cancellation with AbortSignal

You may cancel requests with [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired.
Using AbortController, you can easily implement request timeouts.

###### Node.js

AbortController needs to be introduced in Node.js environment via [abort-controller](https://www.npmjs.com/package/abort-controller) npm package.

```js
const SwaggerClient = require('swagger-client');
const AbortController = require('abort-controller');

const controller = new AbortController();
const { signal } = controller;
const timeout = setTimeout(() => {
controller.abort();
}, 1);

(async () => {
try {
await SwaggerClient.execute({
spec,
pathName: '/users',
method: 'get',
parameters: { q: 'search string' },
securities: { authorized: { BearerAuth: "3492342948239482398" } },
signal,
});
} catch (error) {
if (error.name === 'AbortError') {
console.error('request was aborted');
}
} finally {
clearTimeout(timeout);
}
})();
```

###### Browser

AbortController is part of modern [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
No need to install it explicitly.

```html
<html>
<head>
<script src="//unpkg.com/swagger-client"></script>
<script>
const controller = new AbortController();
const { signal } = controller;
const timeout = setTimeout(() => {
controller.abort();
}, 1);
(async () => {
try {
await SwaggerClient.execute({
spec,
pathName: '/users',
method: 'get',
parameters: { q: 'search string' },
securities: { authorized: { BearerAuth: "3492342948239482398" } },
signal,
});
} catch (error) {
if (error.name === 'AbortError') {
console.error('request was aborted');
}
} finally {
clearTimeout(timeout);
}
})();
</script>
</head>
<body>
check console in browser's dev. tools
</body>
</html>
```

#### Alternate API

It's also possible to call `execute` method from `SwaggerClient` instance.
Expand Down
70 changes: 70 additions & 0 deletions docs/usage/tags-interface.md
Expand Up @@ -267,3 +267,73 @@ SwaggerClient({ url: 'http://petstore.swagger.io/v2/swagger.json' })
</body>
</html>
```

#### Request cancellation with AbortSignal

You may cancel requests with [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
The AbortController interface represents a controller object that allows you to abort one or more Web requests as and when desired.
Using AbortController, you can easily implement request timeouts.

###### Node.js

AbortController needs to be introduced in Node.js environment via [abort-controller](https://www.npmjs.com/package/abort-controller) npm package.

```js
const SwaggerClient = require('swagger-client');
const AbortController = require('abort-controller');

const controller = new AbortController();
const { signal } = controller;
const timeout = setTimeout(() => {
controller.abort();
}, 1);

(async () => {
try {
await new SwaggerClient({ spec })
.then(client => client.apis.default.getUserList({}, { signal }))
} catch (error) {
if (error.name === 'AbortError') {
console.error('request was aborted');
}
} finally {
clearTimeout(timeout);
}
})();
```

###### Browser

AbortController is part of modern [Web APIs](https://developer.mozilla.org/en-US/docs/Web/API/AbortController).
No need to install it explicitly.

```html
<html>
<head>
<script src="//unpkg.com/swagger-client"></script>
<script>
const controller = new AbortController();
const { signal } = controller;
const timeout = setTimeout(() => {
controller.abort();
}, 1);
(async () => {
try {
await new SwaggerClient({ spec })
.then(client => client.apis.default.getUserList({}, { signal }))
} catch (error) {
if (error.name === 'AbortError') {
console.error('request was aborted');
}
} finally {
clearTimeout(timeout);
}
})();
</script>
</head>
<body>
check console in browser's dev. tools
</body>
</html>
```
15 changes: 15 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Expand Up @@ -72,6 +72,7 @@
"@babel/register": "=7.16.5",
"@commitlint/cli": "^15.0.0",
"@commitlint/config-conventional": "^15.0.0",
"abort-controller": "^3.0.0",
"babel-loader": "=8.2.3",
"babel-plugin-lodash": "=3.3.4",
"cross-env": "=7.0.3",
Expand Down
5 changes: 5 additions & 0 deletions src/execute/index.js
Expand Up @@ -98,6 +98,7 @@ export function buildRequest(options) {
server,
serverVariables,
http,
signal,
} = options;

let { parameters, parameterBuilders } = options;
Expand All @@ -123,6 +124,10 @@ export function buildRequest(options) {
cookies: {},
};

if (signal) {
req.signal = signal;
}

if (requestInterceptor) {
req.requestInterceptor = requestInterceptor;
}
Expand Down
43 changes: 43 additions & 0 deletions test/execute/main.js
@@ -1,4 +1,5 @@
import { Readable } from 'stream';
import AbortController from 'abort-controller';

import { execute, buildRequest, self as stubs } from '../../src/execute/index.js';
import { normalizeSwagger } from '../../src/helpers.js';
Expand Down Expand Up @@ -157,6 +158,48 @@ describe('execute', () => {
});
});

test('should allow aborting request during execution', async () => {
// cross-fetch exposes FetchAPI methods onto global
require('cross-fetch/polyfill');

// Given
const spec = {
host: 'swagger.io',
schemes: ['https'],
paths: {
'/one': {
get: {
operationId: 'getMe',
},
},
},
};

const spy = jest.fn().mockImplementation(() => Promise.resolve(new Response('data')));
const controller = new AbortController();
const { signal } = controller;

const response = execute({
userFetch: spy,
spec,
operationId: 'getMe',
signal,
});

controller.abort();
await response;

expect(spy.mock.calls.length).toEqual(1);
expect(spy.mock.calls[0][1]).toEqual({
method: 'GET',
url: 'https://swagger.io/one',
credentials: 'same-origin',
headers: {},
userFetch: spy,
signal,
});
});

test('should include values for query parameters', () => {
// Given
const spec = {
Expand Down
27 changes: 27 additions & 0 deletions test/interfaces.js
@@ -1,3 +1,5 @@
import AbortController from 'abort-controller';

import {
mapTagOperations,
makeApisTagOperationsOperationExecute,
Expand Down Expand Up @@ -108,6 +110,31 @@ describe('intefaces', () => {
});
});

test('should pass signal option to execute', () => {
// Given
const spyMapTagOperations = jest.spyOn(stubs, 'mapTagOperations');
const spyExecute = jest.fn();
makeApisTagOperationsOperationExecute({ execute: spyExecute });
const { cb } = spyMapTagOperations.mock.calls[0][0];

// When
const controller = new AbortController();
const { signal } = controller;
const executer = cb({ pathName: '/one', method: 'GET' });
executer(['param'], { signal });

// Then
expect(spyExecute.mock.calls.length).toEqual(1);
expect(spyExecute.mock.calls[0][0]).toEqual({
spec: undefined,
operationId: undefined,
method: 'GET',
parameters: ['param'],
pathName: '/one',
signal,
});
});

test('should map tagOperations to execute', () => {
const interfaceValue = makeApisTagOperationsOperationExecute({
spec: {
Expand Down

0 comments on commit 7b6bdc2

Please sign in to comment.