Skip to content

Commit

Permalink
Allow configuring additional success statuses in HttpStore
Browse files Browse the repository at this point in the history
Summary:
Changelog:

* **[Feature]** Add `additionalSuccessStatuses` option to `HttpStore`.

Adds a mechanism for specifying HTTP response codes to treat as successful (on top of actual success codes like 200) when reading from/writing to a remote cache server.

It can be useful to tell Metro to ignore certain errors coming from an HTTP cache backend, particularly on write: if the error is, for example, that a concurrent write has happened to the same key, it's conceptually safe to ignore - because we can reasonably assume that the write is coming from another instance of Metro that has the same cache configuration.

Reviewed By: GijsWeterings

Differential Revision: D55752523

fbshipit-source-id: f4953d3151e6d74714690bda9cc43d004db91b79
  • Loading branch information
motiz88 authored and facebook-github-bot committed Apr 8, 2024
1 parent 3e647a6 commit f8f7d55
Show file tree
Hide file tree
Showing 2 changed files with 67 additions and 7 deletions.
15 changes: 13 additions & 2 deletions packages/metro-cache/src/stores/HttpStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ type EndpointOptions = {
ca?: string | $ReadOnlyArray<string> | Buffer | $ReadOnlyArray<Buffer>,
params?: URLSearchParams,
headers?: {[string]: string},
additionalSuccessStatuses?: $ReadOnlyArray<number>,
};

type Endpoint = {
Expand All @@ -44,6 +45,7 @@ type Endpoint = {
params: URLSearchParams,
headers?: {[string]: string},
timeout: number,
additionalSuccessStatuses: $ReadOnlySet<number>,
};

const ZLIB_OPTIONS = {
Expand Down Expand Up @@ -109,6 +111,9 @@ class HttpStore<T> {
params: new URLSearchParams(options.params),
timeout: options.timeout || 5000,
module: uri.protocol === 'http:' ? http : https,
additionalSuccessStatuses: new Set(
options.additionalSuccessStatuses ?? [],
),
};
}

Expand Down Expand Up @@ -142,7 +147,10 @@ class HttpStore<T> {
resolve(null);

return;
} else if (code !== 200) {
} else if (
code !== 200 &&
!this._getEndpoint.additionalSuccessStatuses.has(code)
) {
res.resume();
reject(new HttpError('HTTP error: ' + code, code));

Expand Down Expand Up @@ -215,7 +223,10 @@ class HttpStore<T> {
const req = this._setEndpoint.module.request(options, res => {
const code = res.statusCode;

if (code < 200 || code > 299) {
if (
(code < 200 || code > 299) &&
!this._setEndpoint.additionalSuccessStatuses.has(code)
) {
res.resume();
reject(new HttpError('HTTP error: ' + code, code));

Expand Down
59 changes: 54 additions & 5 deletions packages/metro-cache/src/stores/__tests__/HttpStore-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ describe('HttpStore', () => {
let HttpStore;
let httpPassThrough;

function responseHttpOk(data) {
function responseHttpOk(data, statusCode = 200) {
const res = Object.assign(new PassThrough(), {
statusCode: 200,
statusCode,
});

process.nextTick(() => {
Expand All @@ -30,10 +30,16 @@ describe('HttpStore', () => {
return res;
}

function responseHttpError(code) {
return Object.assign(new PassThrough(), {
statusCode: code,
function responseHttpError(statusCode) {
const res = Object.assign(new PassThrough(), {
statusCode,
});

process.nextTick(() => {
res.end();
});

return res;
}

function responseError(err) {
Expand Down Expand Up @@ -112,6 +118,25 @@ describe('HttpStore', () => {
});
});

it('get() resolves when the HTTP code is in additionalSuccessStatuses', async () => {
const store = new HttpStore({
endpoint: 'http://www.example.com/endpoint',
additionalSuccessStatuses: [419],
});
const promise = store.get(Buffer.from('key'));
const [opts, callback] = require('http').request.mock.calls[0];

expect(opts.method).toEqual('GET');
expect(opts.host).toEqual('www.example.com');
expect(opts.path).toEqual('/endpoint/6b6579');
expect(opts.timeout).toEqual(5000);

callback(responseHttpOk(JSON.stringify({foo: 42}), 419));
jest.runAllTimers();

expect(await promise).toEqual({foo: 42});
});

it('rejects when it gets an invalid JSON response', done => {
const store = new HttpStore({endpoint: 'http://example.com'});
const promise = store.get(Buffer.from('key'));
Expand Down Expand Up @@ -187,6 +212,30 @@ describe('HttpStore', () => {
});
});

test('set() resolves when the HTTP code is in additionalSuccessStatuses', done => {
const store = new HttpStore({
endpoint: 'http://www.example.com/endpoint',
additionalSuccessStatuses: [403],
});
const promise = store.set(Buffer.from('key-set'), {foo: 42});
const [opts, callback] = require('http').request.mock.calls[0];

expect(opts.method).toEqual('PUT');
expect(opts.host).toEqual('www.example.com');
expect(opts.path).toEqual('/endpoint/6b65792d736574');
expect(opts.timeout).toEqual(5000);

callback(responseHttpError(403));

httpPassThrough.on('data', () => {});

httpPassThrough.on('end', async () => {
await promise; // Ensure that the setting promise successfully finishes.

done();
});
});

it('rejects when setting and HTTP returns an error response', done => {
const store = new HttpStore({endpoint: 'http://example.com'});
const promise = store.set(Buffer.from('key-set'), {foo: 42});
Expand Down

0 comments on commit f8f7d55

Please sign in to comment.