Socket
Socket
Sign inDemoInstall

fetch-retry

Package Overview
Dependencies
8
Maintainers
1
Versions
37
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 2.0.0 to 2.1.0

.nycrc.json

66

index.js

@@ -10,18 +10,23 @@ 'use strict';

if (options && options.retries) {
retries = options.retries;
if (options && options.retries !== undefined) {
if (isPositiveInteger(options.retries)) {
retries = options.retries;
} else {
throw new ArgumentError('retries must be a positive integer');
}
}
if (options && options.retryDelay) {
retryDelay = options.retryDelay;
if (options && options.retryDelay !== undefined) {
if (isPositiveInteger(options.retryDelay) || (typeof options.retryDelay === 'function')) {
retryDelay = options.retryDelay;
} else {
throw new ArgumentError('retryDelay must be a positive integer or a function returning a positive integer');
}
}
if (options && options.retryOn) {
if (options.retryOn instanceof Array) {
if (Array.isArray(options.retryOn) || (typeof options.retryOn === 'function')) {
retryOn = options.retryOn;
} else {
throw {
name: 'ArgumentError',
message: 'retryOn property expects an array'
}
throw new ArgumentError('retryOn property expects an array or function');
}

@@ -31,10 +36,16 @@ }

return new Promise(function(resolve, reject) {
var wrappedFetch = function(n) {
var wrappedFetch = function(attempt) {
fetch(url, options)
.then(function(response) {
if (retryOn.indexOf(response.status) === -1) {
if (Array.isArray(retryOn) && retryOn.indexOf(response.status) === -1) {
resolve(response);
} else if (typeof retryOn === 'function') {
if (retryOn(attempt, null, response)) {
retry(attempt, null, response);
} else {
resolve(response);
}
} else {
if (n > 0) {
retry(n);
if (attempt < retries) {
retry(attempt, null, response);
} else {

@@ -46,4 +57,10 @@ resolve(response);

.catch(function(error) {
if (n > 0) {
retry(n);
if (typeof retryOn === 'function') {
if (retryOn(attempt, error, null)) {
retry(attempt, error, null);
} else {
reject(error);
}
} else if (attempt < retries) {
retry(attempt, error, null);
} else {

@@ -55,10 +72,21 @@ reject(error);

function retry(n) {
function retry(attempt, error, response) {
var delay = (typeof retryDelay === 'function') ?
retryDelay(attempt, error, response) : retryDelay;
setTimeout(function() {
wrappedFetch(--n);
}, retryDelay);
wrappedFetch(++attempt);
}, delay);
}
wrappedFetch(retries);
wrappedFetch(0);
});
};
function isPositiveInteger(value) {
return Number.isInteger(value) && value >= 0;
}
function ArgumentError(message) {
this.name = 'ArgumentError';
this.message = message;
}
{
"name": "fetch-retry",
"version": "2.0.0",
"version": "2.1.0",
"description": "Adds retry functionality to the Fetch API",

@@ -8,3 +8,3 @@ "repository": "https://github.com/jonbern/fetch-retry.git",

"scripts": {
"test": "mocha test/**/**.js",
"test": "nyc mocha test/**/**.js",
"integration-test": "mocha test/integration/"

@@ -22,15 +22,17 @@ },

"dependencies": {
"es6-promise": "^4.2.4",
"es6-promise": "^4.2.6",
"isomorphic-fetch": "^2.2.1"
},
"devDependencies": {
"body-parser": "^1.18.3",
"chai": "^4.1.2",
"body-parser": "^1.19.0",
"chai": "^4.2.0",
"chai-as-promised": "^7.1.1",
"eslint": "^5.16.0",
"expectations": "^0.7.1",
"express": "^4.16.2",
"express": "^4.16.4",
"mocha": "^5.2.0",
"proxyquire": "^2.0.1",
"sinon": "^6.1.4"
"nyc": "^14.0.0",
"proxyquire": "^2.1.0",
"sinon": "^6.3.5"
}
}

@@ -16,5 +16,5 @@ # fetch-retry

## Example
`fetch-retry` is used the same way as `fetch`, but also accepts `retries` and `retryDelay` on the `options` object.
`fetch-retry` is used the same way as `fetch`, but also accepts `retries`, `retryDelay`, and `retryOn` on the `options` object.
These properties are optional, and when omitted will default to 3 retries and a 1000ms retry delay.
These properties are optional, and when omitted will default to 3 retries, a 1000ms retry delay, and to retry only on network errors.

@@ -39,4 +39,20 @@ ```javascript

## Example: Exponential backoff
The default behavior of `fetch-retry` is to wait a fixed amount of time between attempts, but it is also possible to customize this by passing a function as the `retryDelay` option. The function is supplied three arguments: `attempt` (starting at 0), `error` (in case of a network error), and `response`. It must return a number indicating the delay.
```javascript
fetch(url, {
retryDelay: function(attempt, error, response) {
return Math.pow(2, attempt) * 1000; // 1000, 2000, 4000
}
}).then(function(response) {
return response.json();
}).then(function(json) {
// do something with the result
console.log(json);
});
```
## Example: Retry on 503 (Service Unavailable)
The default behavior of `fetch-retry` is to only retry requests on network related issues, but it is also possible to configure it to retry on specific HTTP status codes. This is done by using the `retryOn` property, which expects an array of HTTP status codes.
The default behavior of `fetch-retry` is to only retry requests on network related issues, but it is also possible to configure it to retry on specific HTTP status codes. This is done by using the `retryOn` property, which expects an array of HTTP status codes.

@@ -55,1 +71,21 @@ ```javascript

```
## Example: Retry custom behavior
The `retryOn` option may also be specified as a function, in which case it will be supplied three arguments: `attempt` (starting at 0), `error` (in case of a network error), and `response`. Return a truthy value from this function in order to trigger a retry, any falsy value will result in the call to fetch either resolving (in case the last attempt resulted in a response), or rejecting (in case the last attempt resulted in an error).
```javascript
fetch(url, {
retryOn: function(attempt, error, response) {
// retry on any network error, or 4xx or 5xx status codes
if (error !== null || response.status >= 400) {
console.log(`retrying, attempt number ${attempt + 1}`);
return true;
}
})
.then(function(response) {
return response.json();
}).then(function(json) {
// do something with the result
console.log(json);
});
```
'use strict';
const chai = require("chai");
const chaiAsPromised = require("chai-as-promised");
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
chai.use(chaiAsPromised);

@@ -45,3 +45,3 @@ chai.should();

});
}
};

@@ -51,7 +51,7 @@ [200, 503, 404].forEach(statusCode => {

describe('when endpoint returns ' + statusCode, () => {
before(() => {
return setupResponses([statusCode]);
});
it('does not retry request', () => {

@@ -62,7 +62,7 @@ return fetchRetry(baseUrl)

});
});
});
describe('when configured to retry on a specific HTTP code', () => {

@@ -72,3 +72,3 @@

const retryOn = [503]
const retryOn = [503];

@@ -84,3 +84,3 @@ beforeEach(() => {

retryOn
}
};

@@ -99,3 +99,3 @@ const expectedCallCount = options.retries + 1;

retryOn
}
};

@@ -105,3 +105,3 @@ const expectedResponse = {

ok: false
}
};

@@ -122,7 +122,7 @@ return fetchRetry(baseUrl, options)

const retryOnStatus = 503
const retryOnStatus = 503;
const responses = [503, 503, 200];
const requestsToRetry = responses
.filter(response => response === retryOnStatus)
.length;
.filter(response => response === retryOnStatus)
.length;

@@ -138,3 +138,3 @@ beforeEach(() => {

retryOn: [retryOnStatus]
}
};

@@ -153,3 +153,3 @@ const expectedCallCount = requestsToRetry + 1;

retryOn: [retryOnStatus]
}
};

@@ -159,3 +159,3 @@ const expectedResponse = {

ok: true
}
};

@@ -180,3 +180,3 @@ return fetchRetry(baseUrl, options)

const retryOn = [503, 404]
const retryOn = [503, 404];

@@ -192,3 +192,3 @@ beforeEach(() => {

retryOn
}
};

@@ -195,0 +195,0 @@ const expectedCallCount = options.retries + 1;

@@ -10,3 +10,3 @@ const express = require('express');

res.header('Access-Control-Allow-Origin', '*');
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();

@@ -43,2 +43,2 @@ });

server = app.listen(3000);
server = app.listen(3000);

@@ -38,3 +38,3 @@ 'use strict';

fetch = sinon.stub();
global.fetch = sinon.stub();
fetch.onCall(0).returns(deferred1.promise);

@@ -85,3 +85,3 @@ fetch.onCall(1).returns(deferred2.promise);

describe('when #options.retryOn is not an array', () => {
describe('when #options.retryOn is not an array or function', () => {

@@ -94,3 +94,3 @@ it('throws exception', () => {

name: 'ArgumentError',
message: 'retryOn property expects an array'
message: 'retryOn property expects an array or function'
});

@@ -121,44 +121,19 @@ });

describe('when #options.retries=3 (default)', function() {
describe('#options.retries', function() {
beforeEach(function() {
thenCallback = sinon.spy();
catchCallback = sinon.spy();
describe('when #options.retries=3 (default)', function() {
fetchRetry('http://someurl')
.then(thenCallback)
.catch(catchCallback);
});
describe('when first call is a success', function() {
beforeEach(function() {
deferred1.resolve({ status: 200 });
});
thenCallback = sinon.spy();
catchCallback = sinon.spy();
describe('when resolved', function() {
it('invokes the then callback', function() {
expect(thenCallback.called).toBe(true);
});
it('calls fetch once', function() {
expect(fetch.callCount).toBe(1);
});
fetchRetry('http://someurl')
.then(thenCallback)
.catch(catchCallback);
});
});
describe('when first call is a success', function() {
describe('when first call is a failure', function() {
beforeEach(function() {
deferred1.reject();
});
describe('when second call is a succcess', function() {
beforeEach(function() {
clock.tick(delay);
deferred2.resolve({ status: 200 });
deferred1.resolve({ status: 200 });
});

@@ -172,4 +147,4 @@

it('calls fetch twice', function() {
expect(fetch.callCount).toBe(2);
it('calls fetch once', function() {
expect(fetch.callCount).toBe(1);
});

@@ -181,14 +156,13 @@

describe('when second call is a failure', function() {
describe('when first call is a failure', function() {
beforeEach(function() {
deferred2.reject();
clock.tick(delay);
deferred1.reject();
});
describe('when third call is a success', function() {
describe('when second call is a success', function() {
beforeEach(function() {
deferred3.resolve({ status: 200 });
clock.tick(delay);
deferred2.resolve({ status: 200 });
});

@@ -202,4 +176,4 @@

it('calls fetch three times', function() {
expect(fetch.callCount).toBe(3);
it('calls fetch twice', function() {
expect(fetch.callCount).toBe(2);
});

@@ -211,13 +185,13 @@

describe('when third call is a failure', function() {
describe('when second call is a failure', function() {
beforeEach(function() {
deferred3.reject();
deferred2.reject();
clock.tick(delay);
});
describe('when fourth call is a success', function() {
describe('when third call is a success', function() {
beforeEach(function() {
deferred4.resolve({ status: 200 });
deferred3.resolve({ status: 200 });
clock.tick(delay);

@@ -232,4 +206,4 @@ });

it('calls fetch four times', function() {
expect(fetch.callCount).toBe(4);
it('calls fetch three times', function() {
expect(fetch.callCount).toBe(3);
});

@@ -241,17 +215,26 @@

describe('when fourth call is a failure', function() {
describe('when third call is a failure', function() {
beforeEach(function() {
deferred4.reject();
deferred3.reject();
clock.tick(delay);
});
describe('when rejected', function() {
describe('when fourth call is a success', function() {
it('invokes the catch callback', function() {
expect(catchCallback.called).toBe(true);
beforeEach(function() {
deferred4.resolve({ status: 200 });
clock.tick(delay);
});
it('does not call fetch again', function() {
expect(fetch.callCount).toBe(4);
describe('when resolved', function() {
it('invokes the then callback', function() {
expect(thenCallback.called).toBe(true);
});
it('calls fetch four times', function() {
expect(fetch.callCount).toBe(4);
});
});

@@ -261,2 +244,23 @@

describe('when fourth call is a failure', function() {
beforeEach(function() {
deferred4.reject();
clock.tick(delay);
});
describe('when rejected', function() {
it('invokes the catch callback', function() {
expect(catchCallback.called).toBe(true);
});
it('does not call fetch again', function() {
expect(fetch.callCount).toBe(4);
});
});
});
});

@@ -270,31 +274,82 @@

});
describe('when #options.retries=1', function() {
describe('when #options.retries=1', function() {
beforeEach(function() {
thenCallback = sinon.spy();
catchCallback = sinon.spy();
beforeEach(function() {
thenCallback = sinon.spy();
catchCallback = sinon.spy();
fetchRetry('http://someurl', { retries: 1 })
.then(thenCallback)
.catch(catchCallback);
});
fetchRetry('http://someurl', { retries: 1 })
.then(thenCallback)
.catch(catchCallback);
});
describe('when first call is a success', function() {
describe('when first call is a success', function() {
beforeEach(function() {
deferred1.resolve({ status: 200 });
});
beforeEach(function() {
deferred1.resolve({ status: 200 });
describe('when resolved', function() {
it('invokes the then callback', function() {
expect(thenCallback.called).toBe(true);
});
it('calls fetch once', function() {
expect(fetch.callCount).toBe(1);
});
});
});
describe('when resolved', function() {
describe('when first call is a failure', function() {
it('invokes the then callback', function() {
expect(thenCallback.called).toBe(true);
beforeEach(function() {
deferred1.reject();
clock.tick(delay);
});
it('calls fetch once', function() {
expect(fetch.callCount).toBe(1);
describe('when second call is a success', function() {
beforeEach(function() {
deferred2.resolve({ status: 200 });
clock.tick(delay);
});
describe('when resolved', function() {
it('invokes the then callback', function() {
expect(thenCallback.called).toBe(true);
});
it('calls fetch twice', function() {
expect(fetch.callCount).toBe(2);
});
});
});
describe('when second call is a failure', function() {
beforeEach(function() {
deferred2.reject();
clock.tick(delay);
});
describe('when rejected', function() {
it('invokes the catch callback', function() {
expect(catchCallback.called).toBe(true);
});
it('does not call fetch again', function() {
expect(fetch.callCount).toBe(2);
});
});
});
});

@@ -304,14 +359,17 @@

describe('when first call is a failure', function() {
describe('when #options.retries=0', function() {
beforeEach(function() {
deferred1.reject();
clock.tick(delay);
thenCallback = sinon.spy();
catchCallback = sinon.spy();
fetchRetry('http://someurl', { retries: 0 })
.then(thenCallback)
.catch(catchCallback);
});
describe('when second call is a succcess', function() {
describe('when first call is a success', function() {
beforeEach(function() {
deferred2.resolve({ status: 200 });
clock.tick(delay);
deferred1.resolve({ status: 200 });
});

@@ -325,4 +383,4 @@

it('calls fetch twice', function() {
expect(fetch.callCount).toBe(2);
it('calls fetch once', function() {
expect(fetch.callCount).toBe(1);
});

@@ -334,10 +392,9 @@

describe('when second call is a failure', function() {
describe('when first call is a failure', function() {
beforeEach(function() {
deferred2.reject();
clock.tick(delay);
deferred1.reject();
});
describe('when rejected', function() {
describe('when rejected', () => {

@@ -348,6 +405,20 @@ it('invokes the catch callback', function() {

it('does not call fetch again', function() {
expect(fetch.callCount).toBe(2);
});
});
});
});
describe('when #options.retries is not a a positive integer', () => {
['1', -1, 'not a number', null].forEach(invalidRetries => {
it('throws error', () => {
const expectedError = {
name: 'ArgumentError',
message: 'retries must be a positive integer'
};
expect(() => {
fetchRetry('http://someurl', { retries: invalidRetries });
}).toThrow(expectedError);
});

@@ -361,33 +432,88 @@

describe('when #options.retryDelay is provided', function() {
describe('#options.retryDelay', function() {
var options;
var retryDelay;
describe('when #options.retryDelay is a number', function() {
beforeEach(function() {
retryDelay = 5000;
options = {
retryDelay: retryDelay
};
var options;
var retryDelay;
thenCallback = sinon.spy();
beforeEach(function() {
retryDelay = 5000;
options = {
retryDelay: retryDelay
};
fetchRetry('http://someUrl', options)
.then(thenCallback)
thenCallback = sinon.spy();
fetchRetry('http://someUrl', options)
.then(thenCallback);
});
describe('when first call is unsuccessful', function() {
beforeEach(function() {
deferred1.reject();
});
describe('after specified time', function() {
beforeEach(function() {
clock.tick(retryDelay);
});
it('invokes fetch again', function() {
expect(fetch.callCount).toBe(2);
});
});
describe('after less than specified time', function() {
beforeEach(function() {
clock.tick(1000);
});
it('does not invoke fetch again', function() {
expect(fetch.callCount).toBe(1);
});
});
});
});
describe('when first call is unsuccessful', function() {
describe('when #options.retryDelay is 0', function() {
var options;
var retryDelay;
beforeEach(function() {
deferred1.reject();
retryDelay = 0;
options = {
retryDelay: retryDelay
};
thenCallback = sinon.spy();
fetchRetry('http://someUrl', options)
.then(thenCallback);
});
describe('after specified time', function() {
describe('when first call is unsuccessful', function() {
beforeEach(function() {
clock.tick(retryDelay);
deferred1.reject();
});
it('invokes fetch again', function() {
expect(fetch.callCount).toBe(2);
describe('after one event loop tick', function() {
beforeEach(function() {
clock.tick(0);
});
it('invokes fetch again', function() {
expect(fetch.callCount).toBe(2);
});
});

@@ -397,12 +523,85 @@

describe('after less than specified time', function() {
});
describe('when #options.retryDelay is not a a positive integer', () => {
['1', -1, 'not a number', null].forEach(invalidDelay => {
it('throws error', () => {
const expectedError = {
name: 'ArgumentError',
message: 'retryDelay must be a positive integer or a function returning a positive integer'
};
expect(() => {
fetchRetry('http://someurl', { retryDelay: invalidDelay });
}).toThrow(expectedError);
});
});
});
describe('when #options.retryDelay is a function', function() {
var options;
var retryDelay;
beforeEach(function() {
retryDelay = sinon.stub().returns(5000);
options = {
retryDelay: retryDelay
};
thenCallback = sinon.spy();
fetchRetry('http://someUrl', options)
.then(thenCallback);
});
describe('when first call is unsuccessful', function() {
beforeEach(function() {
clock.tick(1000);
deferred1.reject(new Error('first error'));
});
it('does not invoke fetch again', function() {
expect(fetch.callCount).toBe(1);
describe('when the second call is a success', function() {
beforeEach(function() {
deferred2.resolve({ status: 200 });
clock.tick(5000);
});
it('invokes the retryDelay function', function() {
expect(retryDelay.called).toBe(true);
expect(retryDelay.lastCall.args[0]).toEqual(0);
expect(retryDelay.lastCall.args[1].message).toEqual('first error');
});
});
describe('when second call is a failure', function() {
beforeEach(function() {
deferred2.reject(new Error('second error'));
clock.tick(5000);
});
describe('when the third call is a success', function() {
beforeEach(function() {
deferred3.resolve({ status: 200 });
clock.tick(5000);
});
it('invokes the retryDelay function again', function() {
expect(retryDelay.callCount).toBe(2);
expect(retryDelay.lastCall.args[0]).toEqual(1);
expect(retryDelay.lastCall.args[1].message).toEqual('second error');
});
});
});
});

@@ -414,2 +613,263 @@

describe('#options.retryOn', () => {
describe('when #options.retryOn is an array', () => {
var options;
var retryOn;
beforeEach(function() {
retryOn = [503, 404];
options = {
retryOn: retryOn
};
thenCallback = sinon.spy();
catchCallback = sinon.spy();
fetchRetry('http://someUrl', options)
.then(thenCallback)
.catch((catchCallback));
});
describe('when first fetch is resolved with status code specified in retryOn array', () => {
beforeEach(() => {
deferred1.resolve({status: 503});
});
describe('after specified delay', () => {
beforeEach(() => {
clock.tick(delay);
});
it('retries fetch', () => {
expect(fetch.callCount).toBe(2);
});
describe('when second fetch resolves with a different status code', () => {
beforeEach(() => {
deferred2.resolve({status: 200});
});
describe('when resolved', () => {
it('invokes the then callback', function() {
expect(thenCallback.called).toBe(true);
});
it('has called fetch twice', function() {
expect(fetch.callCount).toBe(2);
});
});
});
});
});
});
describe('when #options.retryOn is a function', function() {
var options;
var retryOn;
beforeEach(function() {
retryOn = sinon.stub();
options = {
retryOn: retryOn
};
thenCallback = sinon.spy();
catchCallback = sinon.spy();
fetchRetry('http://someUrl', options)
.then(thenCallback)
.catch((catchCallback));
});
describe('when first attempt is rejected due to network error', function() {
describe('when #retryOn() returns true', () => {
beforeEach(function() {
retryOn.returns(true);
deferred1.reject(new Error('first error'));
});
describe('when rejected', function() {
it('invokes #retryOn function with an error', function() {
expect(retryOn.called).toBe(true);
expect(retryOn.lastCall.args.length).toBe(3);
expect(retryOn.lastCall.args[0]).toBe(0);
expect(retryOn.lastCall.args[1] instanceof Error).toBe(true);
expect(retryOn.lastCall.args[2]).toBe(null);
});
describe('after specified time', function() {
beforeEach(function() {
clock.tick(delay);
});
it('invokes fetch again', function() {
expect(fetch.callCount).toBe(2);
});
describe('when the second call is unsuccessful', function() {
beforeEach(function() {
deferred2.reject(new Error('second error'));
clock.tick(delay);
});
describe('when rejected', function() {
it('invokes the #retryOn function twice', function() {
expect(retryOn.callCount).toBe(2);
expect(retryOn.lastCall.args[0]).toBe(1);
});
});
});
});
});
});
describe('when #retryOn() returns false', () => {
beforeEach(function() {
retryOn.returns(false);
deferred1.reject(new Error('first error'));
});
describe('when rejected', function() {
it('invokes #retryOn function with an error', function() {
expect(retryOn.called).toBe(true);
expect(retryOn.lastCall.args.length).toBe(3);
expect(retryOn.lastCall.args[0]).toBe(0);
expect(retryOn.lastCall.args[1] instanceof Error).toBe(true);
expect(retryOn.lastCall.args[2]).toBe(null);
});
describe('after specified time', function() {
beforeEach(function() {
clock.tick(delay);
});
it('invokes the catch callback', function() {
expect(catchCallback.called).toBe(true);
});
it('does not call fetch again', function() {
expect(fetch.callCount).toBe(1);
});
});
});
});
});
describe('when first attempt is resolved', function() {
describe('when #retryOn() returns true', () => {
beforeEach(function() {
retryOn.returns(true);
deferred1.resolve({ status: 200 });
});
describe('after specified delay', () => {
beforeEach(function() {
clock.tick(delay);
});
it('calls fetch again', function() {
expect(fetch.callCount).toBe(2);
});
describe('when second call is resolved', () => {
beforeEach(function() {
deferred2.resolve({ status: 200 });
clock.tick(delay);
});
it('invokes the #retryOn function with the response', function() {
expect(retryOn.called).toBe(true);
expect(retryOn.lastCall.args.length).toBe(3);
expect(retryOn.lastCall.args[0]).toBe(0);
expect(retryOn.lastCall.args[1]).toBe(null);
expect(retryOn.lastCall.args[2]).toEqual({ status: 200 });
});
});
});
});
describe('when #retryOn() returns false', () => {
beforeEach(function() {
retryOn.returns(false);
deferred1.resolve({ status: 502 });
});
describe('when resolved', () => {
it('invokes the then callback', function() {
expect(thenCallback.called).toBe(true);
});
it('calls fetch 1 time only', function() {
expect(fetch.callCount).toBe(1);
});
});
});
});
});
describe('when #options.retryOn is not an array or function', function() {
var options;
describe('when #options.retryOn is not an array or function', () => {
it('throws exception', () => {
expect(function() {
options.retryOn = 503;
fetchRetry('http://someUrl', options);
}).toThrow({
name: 'ArgumentError',
message: 'retryOn property expects an array or function'
});
});
});
});
});
});

@@ -416,0 +876,0 @@

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc