shopify-api-node
Advanced tools
Comparing version 3.9.1 to 3.10.0
243
index.js
@@ -12,2 +12,17 @@ 'use strict'; | ||
const retryableErrorCodes = new Set([ | ||
'ETIMEDOUT', | ||
'ECONNRESET', | ||
'EADDRINUSE', | ||
'ECONNREFUSED', | ||
'EPIPE', | ||
'ENOTFOUND', | ||
'ENETUNREACH', | ||
'EAI_AGAIN' | ||
]); | ||
const retryableStatusCodes = new Set([ | ||
408, 413, 429, 500, 502, 503, 504, 521, 522, 524 | ||
]); | ||
/** | ||
@@ -29,2 +44,4 @@ * Creates a Shopify instance. | ||
* JSON | ||
* @param {Number} [options.maxRetries] Maximum number of automatic request | ||
* retries | ||
* @constructor | ||
@@ -39,3 +56,4 @@ * @public | ||
(!options.accessToken && (!options.apiKey || !options.password)) || | ||
(options.accessToken && (options.apiKey || options.password)) | ||
(options.accessToken && (options.apiKey || options.password)) || | ||
(options.autoLimit && options.maxRetries) | ||
) { | ||
@@ -50,2 +68,3 @@ throw new Error('Missing or invalid options'); | ||
timeout: 60000, | ||
maxRetries: 0, | ||
...options | ||
@@ -141,4 +160,20 @@ }; | ||
responseType: 'json', | ||
retry: 0, | ||
method | ||
retry: | ||
this.options.maxRetries > 0 | ||
? { | ||
limit: this.options.maxRetries, | ||
// Don't clamp Shopify `Retry-After` header values too low. | ||
maxRetryAfter: Infinity, | ||
calculateDelay | ||
} | ||
: 0, | ||
method, | ||
hooks: { | ||
afterResponse: [ | ||
(res) => { | ||
this.updateLimits(res.headers['x-shopify-shop-api-call-limit']); | ||
return res; | ||
} | ||
] | ||
} | ||
}; | ||
@@ -150,49 +185,38 @@ | ||
return got(uri, options).then( | ||
(res) => { | ||
const body = res.body; | ||
return got(uri, options).then((res) => { | ||
const body = res.body; | ||
this.updateLimits(res.headers['x-shopify-shop-api-call-limit']); | ||
if (res.statusCode === 202 && res.headers['location']) { | ||
const retryAfter = res.headers['retry-after'] * 1000 || 0; | ||
const { pathname, search } = url.parse(res.headers['location']); | ||
if (res.statusCode === 202 && res.headers['location']) { | ||
const retryAfter = res.headers['retry-after'] * 1000 || 0; | ||
const { pathname, search } = url.parse(res.headers['location']); | ||
return delay(retryAfter).then(() => { | ||
const uri = { pathname, ...this.baseUrl }; | ||
return delay(retryAfter).then(() => { | ||
const uri = { pathname, ...this.baseUrl }; | ||
if (search) uri.search = search; | ||
if (search) uri.search = search; | ||
return this.request(uri, 'GET', key); | ||
}); | ||
} | ||
return this.request(uri, 'GET', key); | ||
const data = key ? body[key] : body || {}; | ||
if (res.headers.link) { | ||
const link = parseLinkHeader(res.headers.link); | ||
if (link.next) { | ||
Object.defineProperties(data, { | ||
nextPageParameters: { value: link.next.query } | ||
}); | ||
} | ||
const data = key ? body[key] : body || {}; | ||
if (res.headers.link) { | ||
const link = parseLinkHeader(res.headers.link); | ||
if (link.next) { | ||
Object.defineProperties(data, { | ||
nextPageParameters: { value: link.next.query } | ||
}); | ||
} | ||
if (link.previous) { | ||
Object.defineProperties(data, { | ||
previousPageParameters: { value: link.previous.query } | ||
}); | ||
} | ||
if (link.previous) { | ||
Object.defineProperties(data, { | ||
previousPageParameters: { value: link.previous.query } | ||
}); | ||
} | ||
} | ||
return data; | ||
}, | ||
(err) => { | ||
this.updateLimits( | ||
err.response && err.response.headers['x-shopify-shop-api-call-limit'] | ||
); | ||
return Promise.reject(err); | ||
} | ||
); | ||
return data; | ||
}); | ||
}; | ||
@@ -252,26 +276,35 @@ | ||
responseType: 'json', | ||
retry: 0, | ||
method: 'POST', | ||
body: json ? this.options.stringifyJson({ query: data, variables }) : data | ||
}; | ||
body: json ? this.options.stringifyJson({ query: data, variables }) : data, | ||
retry: | ||
this.options.maxRetries > 0 | ||
? { | ||
limit: this.options.maxRetries, | ||
// Don't clamp Shopify `Retry-After` header values too low. | ||
maxRetryAfter: Infinity, | ||
calculateDelay | ||
} | ||
: 0, | ||
hooks: { | ||
afterResponse: [ | ||
(res) => { | ||
if (res.body) { | ||
if (res.body.extensions && res.body.extensions.cost) { | ||
this.updateGraphqlLimits(res.body.extensions.cost); | ||
} | ||
return got(uri, options).then((res) => { | ||
if (res.body.extensions && res.body.extensions.cost) { | ||
this.updateGraphqlLimits(res.body.extensions.cost); | ||
} | ||
if (res.body.errors) { | ||
// Make Got consider this response errored and retry if needed. | ||
throw new Error(res.body.errors[0].message); | ||
} | ||
} | ||
if (res.body.errors) { | ||
const first = res.body.errors[0]; | ||
const err = new Error(first.message); | ||
err.locations = first.locations; | ||
err.path = first.path; | ||
err.extensions = first.extensions; | ||
err.response = res; | ||
throw err; | ||
return res; | ||
} | ||
], | ||
beforeError: [decorateError] | ||
} | ||
}; | ||
return res.body.data || {}; | ||
}); | ||
return got(uri, options).then(responseData); | ||
}; | ||
@@ -282,2 +315,34 @@ | ||
/** | ||
* Got `calculateDelay` hook function passed to decide how long to wait before | ||
* retrying. | ||
* | ||
* @param {Object} retryObject Got's input for the retry logic | ||
* @return {Number} The delay | ||
* @private | ||
*/ | ||
function calculateDelay(retryObject) { | ||
return maybeRetryMS(retryObject.error) || retryObject.computedValue; | ||
} | ||
/** | ||
* Decorates an `Error` object with details from GraphQL errors in the response | ||
* body. | ||
* | ||
* @param {Error} error The error to decorate | ||
* @return {Error} The decorated error | ||
* @private | ||
*/ | ||
function decorateError(error) { | ||
if (error.response && error.response.body.errors) { | ||
const first = error.response.body.errors[0]; | ||
error.locations = first.locations; | ||
error.path = first.path; | ||
error.extensions = first.extensions; | ||
} | ||
return error; | ||
} | ||
/** | ||
* Returns a promise that resolves after a given amount of time. | ||
@@ -294,2 +359,51 @@ * | ||
/** | ||
* Given an error from Got, see if Shopify told us how long to wait before | ||
* retrying. | ||
* | ||
* @param {Object} error Error object from Got call | ||
* @return {(Number|null)} The duration in ms, or `null` | ||
* @private | ||
**/ | ||
function maybeRetryMS(error) { | ||
// For simplicity, retry network connectivity issues after a hardcoded 1s. | ||
if (retryableErrorCodes.has(error.code)) { | ||
return 1000; | ||
} | ||
const response = error.response; | ||
if (response.headers && response.headers['retry-after']) { | ||
return response.headers['retry-after'] * 1000 || null; | ||
} | ||
if (retryableStatusCodes.has(response.statusCode)) { | ||
// Arbitrary 2 seconds, in case we get a 429 without a `Retry-After` | ||
// response header, or 4xx/5xx series error that matches the Got retry | ||
// defaults. | ||
return 2 * 1000; | ||
} | ||
// Detect GraphQL request throttling. | ||
if (response.body && typeof response.body === 'object') { | ||
const body = response.body; | ||
if ( | ||
body.errors && | ||
body.errors[0].extensions && | ||
body.errors[0].extensions.code == 'THROTTLED' | ||
) { | ||
const costData = body.extensions.cost; | ||
return ( | ||
((costData.requestedQueryCost - | ||
costData.throttleStatus.currentlyAvailable) / | ||
costData.throttleStatus.restoreRate) * | ||
1000 | ||
); | ||
} | ||
} | ||
return null; | ||
} | ||
/** | ||
* Parses the `Link` header into an object. | ||
@@ -325,2 +439,13 @@ * | ||
/** | ||
* Returns the data of a GraphQL response object. | ||
* | ||
* @param {Response} res Got response object | ||
* @return {Object} The data | ||
* @private | ||
*/ | ||
function responseData(res) { | ||
return res.body.data; | ||
} | ||
module.exports = Shopify; |
{ | ||
"name": "shopify-api-node", | ||
"version": "3.9.1", | ||
"version": "3.10.0", | ||
"description": "Shopify API bindings for Node.js", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -55,3 +55,3 @@ # Shopify API Node.js | ||
set to `true` requests are limited as specified in the above example. Defaults | ||
to `false`. | ||
to `false`. Mutually exclusive with the `maxRetries` option. | ||
- `parseJson` - Optional - The function used to parse JSON. The function is | ||
@@ -70,2 +70,9 @@ passed a single argument. This option allows the use of a custom JSON parser | ||
to `60000`, or 1 minute. | ||
- `maxRetries` - Optional - The number of times to attempt to make the request | ||
to Shopify before giving up. Defaults to `0`, which means no automatic | ||
retries. If set to a value greater than `0`, `shopify-api-node` will make up | ||
to that many retries. `shopify-api-node` will respect the `Retry-After` header | ||
for requests to the REST API, and the throttled cost data for requests to the | ||
GraphQL API, and retry the request after that time has elapsed. Mutually | ||
exclusive with the `autoLimit` option. | ||
@@ -261,2 +268,31 @@ #### Return value | ||
## Shopify rate limit avoidance | ||
`shopify-api-node` has two optional mechanisms for avoiding requests failing | ||
with `429 Rate Limit Exceeded` errors from Shopify. | ||
The `autoLimit` option implements a client side leaky bucket algorithm for | ||
delaying requests until Shopify is likely to accept them. When `autoLimit` is | ||
on, each `Shopify` instance will track how many requests have been made, and | ||
delay sending subsequent requests if the rate limit has been exceeded. | ||
`autoLimit` is very efficient because it almost entirely avoids sending requests | ||
which will return 429 errors, but, it does not coordinate between multiple | ||
`Shopify` instances or across multiple processes. If you're using | ||
`shopify-api-node` in many different processes, `autoLimit` will not correctly | ||
avoid 429 errors. | ||
The `maxRetries` option implements a retry based strategy for getting requests | ||
to Shopify, where when a 429 error occurs, the request is automatically retried | ||
after waiting. Shopify usually replies with a `Retry-After` header indicating to | ||
the client when the rate limit is available, and so `shopify-api-node` will wait | ||
that long before retrying. If you are using `shopify-api-node` in many different | ||
processes, they will all be competing to use the same rate limit shopify | ||
enforces, so there is no guarantee that retrying after the `Retry-After` header | ||
delay will work. It is recommended to set `maxRetries` to a high value like `10` | ||
if you are making many concurrent requests in many processes to ensure each | ||
request is retried for long enough to succeed. | ||
`autoLimit` and `maxRetries` can't be used simultaneously. Both are off by | ||
default. | ||
## Available resources and methods | ||
@@ -458,3 +494,3 @@ | ||
- `update(id, params)` | ||
- [giftCardAdjustment](https://help.shopify.com/en/api/reference/plus/gift_card_adjustment) | ||
- [giftCardAdjustment](https://shopify.dev/api/admin-rest/2022-04/resources/gift-card-adjustment) | ||
- `create(giftCardId, params)` | ||
@@ -554,3 +590,3 @@ - `get(giftCardId, id)` | ||
- `productIds([params])` | ||
- [productResourceFeedback](https://help.shopify.com/en/api/reference/sales-channels/productresourcefeedback) | ||
- [productResourceFeedback](https://shopify.dev/api/admin-rest/2022-04/resources/product-resourcefeedback) | ||
- `create(productId[, params])` | ||
@@ -651,4 +687,4 @@ - `list(productId)` | ||
where `params` is a plain JavaScript object. See | ||
https://help.shopify.com/api/reference?ref=microapps for parameters details. | ||
where `params` is a plain JavaScript object. See the [Rest Admin API | ||
reference][reading-api-docs] for parameters details. | ||
@@ -685,4 +721,2 @@ ## GraphQL | ||
- [Becoming a Shopify App Developer][becoming-a-shopify-app-developer] | ||
- [Checking out the roots][checking-out-the-roots] | ||
- [Talking To Other Masters][talking-to-other-masters] | ||
@@ -731,17 +765,11 @@ - [Reading API Docs][reading-api-docs] | ||
[generate-private-app-credentials]: | ||
https://help.shopify.com/api/guides/api-credentials#generate-private-app-credentials?ref=microapps | ||
[oauth]: https://help.shopify.com/api/guides/authentication/oauth?ref=microapps | ||
https://shopify.dev/apps/auth/basic-http#step-2-generate-api-credentials | ||
[oauth]: https://shopify.dev/apps/auth/oauth | ||
[shopify-token]: https://github.com/lpinca/shopify-token | ||
[api-call-limit]: | ||
https://help.shopify.com/api/guides/api-call-limit/?ref=microapps | ||
[api-versioning]: https://help.shopify.com/en/api/versioning | ||
[becoming-a-shopify-app-developer]: | ||
https://app.shopify.com/services/partners/signup?ref=microapps | ||
[checking-out-the-roots]: https://help.shopify.com/api/guides?ref=microapps | ||
[talking-to-other-masters]: | ||
https://ecommerce.shopify.com/c/shopify-apps?ref=microapps | ||
[reading-api-docs]: https://help.shopify.com/api/reference/?ref=microapps | ||
[api-call-limit]: https://shopify.dev/api/usage/rate-limits | ||
[api-versioning]: https://shopify.dev/api/usage/versioning | ||
[talking-to-other-masters]: https://community.shopify.com/ | ||
[reading-api-docs]: https://shopify.dev/api/admin-rest | ||
[learning-from-others]: https://stackoverflow.com/questions/tagged/shopify | ||
[paginated-rest-results]: | ||
https://help.shopify.com/en/api/guides/paginated-rest-results | ||
[paginated-rest-results]: https://shopify.dev/api/usage/pagination-rest | ||
[polaris]: https://polaris.shopify.com/?ref=microapps | ||
@@ -748,0 +776,0 @@ [microapps]: |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
202957
6267
782