Skip to content
This repository was archived by the owner on Oct 10, 2022. It is now read-only.

Commit 8d2162b

Browse files
authored
Merge pull request #32 from netlify/fix-rate-limit
Fix rate limit
2 parents 7dd153e + f8e31cc commit 8d2162b

File tree

7 files changed

+30
-15
lines changed

7 files changed

+30
-15
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ Every open-api method has the following signature:
5151

5252
Perform a call to the given endpoint corresponding with the `operationId`. Returns promise that will resolve with the body of the response, or reject with an error with details about the request attached. Rejects if the `status` > 400. Successful response objects have `status` and `statusText` properties on their prototype.
5353

54-
- `params` is an object that includes any of the required or optional endpoint parameters.
55-
- `params.body` should be an object which gets serialized to JSON automatically.
56-
- If the endpoint accepts `binary`, `params.body` can be a Node.js readable stream.
54+
- `params` is an object that includes any of the required or optional endpoint parameters.
55+
- `params.body` should be an object which gets serialized to JSON automatically.
56+
- If the endpoint accepts `binary`, `params.body` can be a Node.js readable stream or stream ctor (e.g. `() => fs.createReadStream('./foo')`). A Stream ctor function is required to support rate limit retry.
5757

5858
```js
5959
// example params
@@ -125,7 +125,7 @@ async function login () {
125125
// Open browser for authentication
126126
await openBrowser(`https://app.netlify.com/authorize?response_type=ticket&ticket=${ticket.id}`)
127127
const accessToken = await api.getAccessToken(ticket)
128-
// API is also set up to use the returned access token as a side effect
128+
// API is also set up to use the returned access token as a side effect
129129
return accessToken // Save this for later so you can quickly set up an authenticated client
130130
}
131131
```

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"folder-walker": "^3.2.0",
4444
"from2-array": "0.0.4",
4545
"hasha": "^3.0.0",
46+
"is-stream": "^1.1.0",
4647
"lodash.camelcase": "^4.3.0",
4748
"lodash.flatten": "^4.4.0",
4849
"lodash.get": "^4.4.2",

src/deploy/hash-fns.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ async function hashFns(dir, opts) {
1414
assetType: 'function',
1515
hashAlgorithm: 'sha256',
1616
// tmpDir,
17-
statusCb: () => {}
17+
statusCb: () => { }
1818
},
1919
opts
2020
)
@@ -36,7 +36,7 @@ async function hashFns(dir, opts) {
3636
const manifestCollector = manifestCollectorCtor(functions, fnShaMap, opts)
3737

3838
// TODO: Zip up functions, hash then send.
39-
// This is totally wrong wright now.
39+
// This is totally wrong right now.
4040
// See https://github.com/netlify/open-api/blob/master/go/porcelain/deploy.go#L544
4141

4242
await pump(fileStream, fnStat, fnFilter, hasher, manifestCollector)

src/deploy/upload-files.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ async function uploadFiles(api, deployId, uploadList, { concurrentUpload, status
1313

1414
const uploadFile = async (fileObj, index) => {
1515
const { normalizedPath, assetType, runtime } = fileObj
16-
const readStream = fs.createReadStream(fileObj.filepath)
16+
const readStreamCtor = () => fs.createReadStream(fileObj.filepath)
1717

1818
statusCb({
1919
type: 'upload',
@@ -26,7 +26,7 @@ async function uploadFiles(api, deployId, uploadList, { concurrentUpload, status
2626
response = await retryUpload(
2727
() =>
2828
api.uploadDeployFile({
29-
body: readStream,
29+
body: readStreamCtor,
3030
deployId,
3131
path: encodeURI(normalizedPath)
3232
}),
@@ -38,7 +38,7 @@ async function uploadFiles(api, deployId, uploadList, { concurrentUpload, status
3838
response = await await retryUpload(
3939
() =>
4040
api.uploadDeployFunction({
41-
body: readStream,
41+
body: readStreamCtor,
4242
deployId,
4343
name: encodeURI(normalizedPath),
4444
runtime

src/index.test.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ const NetlifyAPI = require('./index')
55
const body = promisify(require('body'))
66
const fromString = require('from2-string')
77
const { TextHTTPError } = require('micro-api-client')
8-
const { existy } = require('./open-api/util')
8+
const { existy, unixNow } = require('./open-api/util')
99

1010
const createServer = handler => {
1111
const s = http.createServer(handler)
@@ -252,11 +252,12 @@ test.serial('test rate-limiting', async t => {
252252
try {
253253
server = createServer(async (req, res) => {
254254
if (!existy(retryAt)) {
255-
retryAt = Date.now() + randomInt(1000, 5000) //ms
255+
retryAt = unixNow() + randomInt(1, 5) //ms
256256

257257
requestRateLimit(res, retryAt)
258258
} else {
259-
const rateLimitFinished = Date.now() >= retryAt
259+
const now = unixNow()
260+
const rateLimitFinished = now >= retryAt
260261

261262
if (rateLimitFinished) {
262263
t.pass('The client made a request at or after the rate limit deadline')

src/open-api/index.js

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ const Headers = fetch.Headers
77
const camelCase = require('lodash.camelcase')
88
const { JSONHTTPError, TextHTTPError } = require('micro-api-client')
99
const debug = require('debug')('netlify:open-api')
10-
const { existy, sleep } = require('./util')
10+
const { existy, sleep, unixNow } = require('./util')
11+
const isStream = require('is-stream')
1112

1213
exports.methods = require('./shape-swagger')
1314

@@ -92,7 +93,15 @@ exports.generateMethod = method => {
9293
let response
9394
try {
9495
debug(`requesting ${path}`)
95-
response = await fetch(path, opts)
96+
97+
if (isStream(opts.body) && opts.body.closed) {
98+
throw new Error('Body readable stream is already used. Try passing a stream ctor instead')
99+
}
100+
101+
// Pass in a function for readable stream ctors
102+
const fetchOpts = typeof opts.body === 'function' ? Object.assign({}, opts, { body: opts.body() }) : opts
103+
104+
response = await fetch(path, fetchOpts)
96105
} catch (e) {
97106
// TODO: clean up this error path
98107
debug(`fetch error`)
@@ -128,7 +137,9 @@ exports.generateMethod = method => {
128137
throw new Error('Header missing reset time')
129138
}
130139
debug(`resetTime: ${resetTime}`)
131-
const sleepTime = resetTime - Date.now()
140+
const now = unixNow()
141+
debug(`unixNow(): ${now}`)
142+
const sleepTime = (resetTime - now < 0 ? 0 : resetTime - now) * 1000
132143
debug(`sleeping for ${sleepTime}ms`)
133144
await sleep(sleepTime)
134145
} catch (e) {

src/open-api/util.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,5 @@ exports.existy = val => val != null
3434

3535
// Async sleep. A whole new WORLD!
3636
exports.sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
37+
38+
exports.unixNow = () => Math.floor(new Date() / 1000)

0 commit comments

Comments
 (0)