Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[feat] Timeouts - response (TTFB) and deadline (total time) #1254

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ We'll be consolidating that soon. Just giving you the heads up. You may see refe
### Usage

##### Prerequisites
- Runtime:
- browser: es5 compatible. IE11+
- Runtime:
- browser: es5 compatible. IE11+
- node v4.x.x
- Building
- node v6.x.x
- node v6.x.x

##### Download via npm

Expand All @@ -34,7 +34,7 @@ npm install swagger-client
```javascript
import Swagger from 'swagger-client'
// Or commonjs
const Swagger = require('swagger-client')
const Swagger = require('swagger-client')
```

##### Import in browser
Expand Down Expand Up @@ -134,7 +134,7 @@ const params = {

parameters, // _named_ parameters in an object, eg: { petId: 'abc' }
securities, // _named_ securities, will only be added to the request, if the spec indicates it. eg: {apiKey: 'abc'}
requestContentType,
requestContentType,
responseContentType,

(http), // You can also override the HTTP client completely
Expand Down Expand Up @@ -166,11 +166,11 @@ Swagger('http://petstore.swagger.io/v2/swagger.json')
client.originalSpec // In case you need it
client.errors // Any resolver errors

// Tags interface
// Tags interface
client.apis.pet.addPet({id: 1, name: "bobby"}).then(...)

// TryItOut Executor, with the `spec` already provided
client.execute({operationId: 'addPet', parameters: {id: 1, name: "bobby") }).then(...)
client.execute({operationId: 'addPet', parameters: {id: 1, name: "bobby") }).then(...)
})

```
Expand All @@ -190,14 +190,14 @@ Swagger({...}).then((client) => {
.apis
.pet // tag name == `pet`
.addPet({id: 1, name: "bobby"}) // operationId == `addPet`
.then(...)
.then(...)
})
```

In Browser
In Browser
----------

Prepare swagger-client.js by `npm run build-bundle`
Prepare swagger-client.js by `npm run build-bundle`
Note, browser version exports class `SwaggerClient` to global namespace
If you need activate CORS requests, just enable it by `withCredentials` property at `http`

Expand All @@ -217,11 +217,11 @@ var swaggerClient = new SwaggerClient(specUrl)
console.error("failed to load the spec" + reason);
})
.then(function(addPetResult) {
console.log(addPetResult.obj);
console.log(addPetResult.obj);
// you may return more promises, if necessary
}, function (reason) {
console.error("failed on API call " + reason);
});
});
})
</script>
</head>
Expand All @@ -232,6 +232,20 @@ var swaggerClient = new SwaggerClient(specUrl)

```

Timeout
-------

You can set a `deadline` timeout which is the *total time* for a response to complete,
as well as `response` timeout which is the *time to first byte* (TTFB). Simply specify
`timeoutDeadline` and/or `timeoutResponse` in your `http` call or in your `Swagger` constructor.


```javascript
http({
url: 'http://swagger.io',
timeoutDeadline: 200
})
```

Compatibility
-------------
Expand Down Expand Up @@ -286,7 +300,7 @@ npm run build-bundle # build browser version available at .../browser

# Migration from 2.x

There has been a complete overhaul of the codebase.
There has been a complete overhaul of the codebase.
For notes about how to migrate coming from 2.x,
please see [Migration from 2.x](docs/MIGRATION_2_X.md)

Expand Down
20 changes: 17 additions & 3 deletions src/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'cross-fetch/polyfill'
import qs from 'qs'
import jsYaml from 'js-yaml'
import isString from 'lodash/isString'
import get from 'lodash/get'
import * as timeout from './timeout'

// For testing
export const self = {
Expand Down Expand Up @@ -36,15 +38,27 @@ export default function http(url, request = {}) {
delete request.headers['Content-Type']
}

// eslint-disable-next-line no-undef
return (request.userFetch || fetch)(request.url, request).then((res) => {
const serialized = self.serializeRes(res, url, request).then((_res) => {
const timeoutResponse = request.timeoutResponse || get(this, 'timeoutResponse', undefined)
let timeoutDeadline = request.timeoutDeadline || get(this, 'timeoutDeadline', undefined)
if ((timeoutResponse !== undefined) && (timeoutDeadline !== undefined)) {
if (timeoutDeadline < timeoutResponse) {
timeoutDeadline = timeoutResponse
}
}
const timeStart = Date.now()
// eslint-disable-next-line no-undef
const reqpromise = (request.userFetch || fetch)(request.url, request)
return timeout.responseTimeout(timeoutResponse, reqpromise)
.then((res) => {
const serializePromise = self.serializeRes(res, url, request).then((_res) => {
if (request.responseInterceptor) {
_res = request.responseInterceptor(_res) || _res
}
return _res
})

const serialized = timeout.deadlineTimeout(timeStart, timeoutDeadline, serializePromise)

if (!res.ok) {
const error = new Error(res.statusText)
error.statusCode = error.status = res.status
Expand Down
52 changes: 52 additions & 0 deletions src/timeout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const makeResponseTimeoutError = function (ms) {
// https://github.com/visionmedia/superagent/blob/master/lib/request-base.js#L666
const err = new Error(`Response timeout of ${ms}ms exceeded`)
err.code = 'ECONNABORTED'
err.errno = 'ETIMEDOUT'
return err
}
const makeDeadlineTimeoutError = function (ms) {
const err = new Error(`Timeout of ${ms}ms exceeded`)
err.code = 'ECONNABORTED'
err.errno = 'ETIME'
return err
}

export function responseTimeout(ms, promise) {
return new Promise((resolve, reject) => {
let timerId
if (ms !== undefined) {
timerId = setTimeout(() => {
reject(makeResponseTimeoutError(ms))
}, ms)
}
promise.then((val) => {
clearTimeout(timerId)
resolve(val)
}, reject)
})
}

export function deadlineTimeout(timeStart, timeoutDeadline, promise) {
const timeTtfb = Date.now()
const timeElapsed = (timeTtfb - timeStart)
let ms // remaining ms
if (timeoutDeadline !== undefined) {
ms = timeoutDeadline - timeElapsed
}
return new Promise((resolve, reject) => {
let timerId
if (ms !== undefined) {
if (ms < 0) {
return reject(makeDeadlineTimeoutError(timeoutDeadline))
}
timerId = setTimeout(() => {
reject(makeDeadlineTimeoutError(timeoutDeadline))
}, ms)
}
promise.then((val) => {
clearTimeout(timerId)
resolve(val)
}, reject)
})
}
46 changes: 46 additions & 0 deletions test/http.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,52 @@ describe('http', () => {
})
})

it('should fail timeout - total response is not received by deadline', (done) => {
xapp = xmock()
xapp.get('http://swagger.io', (req, res) => {
setTimeout(() => {
res.send('hi')
}, 250)
})

return http({
url: 'http://swagger.io',
timeoutDeadline: 200
})
.then(() => {
done(new Error('expected error'))
})
.catch((e) => {
expect(e).toExist()
expect(e.message).toEqual('Timeout of 200ms exceeded')
expect(e.code).toEqual('ECONNABORTED')
done()
})
})

it('should fail timeout - TTFB time to first byte response is not received', (done) => {
xapp = xmock()
xapp.get('http://swagger.io', (req, res) => {
setTimeout(() => {
res.write('hi')
}, 150)
})

return http({
url: 'http://swagger.io',
timeoutResponse: 100
})
.then(() => {
done(new Error('expected error'))
})
.catch((e) => {
expect(e).toExist()
expect(e.message).toEqual('Response timeout of 100ms exceeded')
expect(e.code).toEqual('ECONNABORTED')
done()
})
})

it('should always load a spec as text', () => {
xapp = xmock()
xapp.get('http://swagger.io/somespec', (req, res) => {
Expand Down