Skip to content

Commit

Permalink
[feat] timeout response, timeout deadline
Browse files Browse the repository at this point in the history
  • Loading branch information
rook2pawn committed Mar 5, 2018
1 parent 28fbea2 commit 9c33c49
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 16 deletions.
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

0 comments on commit 9c33c49

Please sign in to comment.