Skip to content

Commit 8d36cce

Browse files
committed
fix: Correctly limit Buffer requests
1 parent 688a986 commit 8d36cce

File tree

7 files changed

+121
-95
lines changed

7 files changed

+121
-95
lines changed

packages/browser/src/transports/fetch.ts

Lines changed: 18 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -133,23 +133,24 @@ export class FetchTransport extends BaseTransport {
133133
}
134134

135135
return this._buffer.add(
136-
new SyncPromise<Response>((resolve, reject) => {
137-
void this._fetch(sentryRequest.url, options)
138-
.then(response => {
139-
const headers = {
140-
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
141-
'retry-after': response.headers.get('Retry-After'),
142-
};
143-
this._handleResponse({
144-
requestType: sentryRequest.type,
145-
response,
146-
headers,
147-
resolve,
148-
reject,
149-
});
150-
})
151-
.catch(reject);
152-
}),
136+
() =>
137+
new SyncPromise<Response>((resolve, reject) => {
138+
void this._fetch(sentryRequest.url, options)
139+
.then(response => {
140+
const headers = {
141+
'x-sentry-rate-limits': response.headers.get('X-Sentry-Rate-Limits'),
142+
'retry-after': response.headers.get('Retry-After'),
143+
};
144+
this._handleResponse({
145+
requestType: sentryRequest.type,
146+
response,
147+
headers,
148+
resolve,
149+
reject,
150+
});
151+
})
152+
.catch(reject);
153+
}),
153154
);
154155
}
155156
}

packages/browser/src/transports/xhr.ts

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,28 @@ export class XHRTransport extends BaseTransport {
3737
}
3838

3939
return this._buffer.add(
40-
new SyncPromise<Response>((resolve, reject) => {
41-
const request = new XMLHttpRequest();
40+
() =>
41+
new SyncPromise<Response>((resolve, reject) => {
42+
const request = new XMLHttpRequest();
4243

43-
request.onreadystatechange = (): void => {
44-
if (request.readyState === 4) {
45-
const headers = {
46-
'x-sentry-rate-limits': request.getResponseHeader('X-Sentry-Rate-Limits'),
47-
'retry-after': request.getResponseHeader('Retry-After'),
48-
};
49-
this._handleResponse({ requestType: sentryRequest.type, response: request, headers, resolve, reject });
50-
}
51-
};
44+
request.onreadystatechange = (): void => {
45+
if (request.readyState === 4) {
46+
const headers = {
47+
'x-sentry-rate-limits': request.getResponseHeader('X-Sentry-Rate-Limits'),
48+
'retry-after': request.getResponseHeader('Retry-After'),
49+
};
50+
this._handleResponse({ requestType: sentryRequest.type, response: request, headers, resolve, reject });
51+
}
52+
};
5253

53-
request.open('POST', sentryRequest.url);
54-
for (const header in this.options.headers) {
55-
if (this.options.headers.hasOwnProperty(header)) {
56-
request.setRequestHeader(header, this.options.headers[header]);
54+
request.open('POST', sentryRequest.url);
55+
for (const header in this.options.headers) {
56+
if (this.options.headers.hasOwnProperty(header)) {
57+
request.setRequestHeader(header, this.options.headers[header]);
58+
}
5759
}
58-
}
59-
request.send(sentryRequest.body);
60-
}),
60+
request.send(sentryRequest.body);
61+
}),
6162
);
6263
}
6364
}

packages/browser/test/unit/mocks/simpletransport.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { BaseTransport } from '../../../src/transports';
55

66
export class SimpleTransport extends BaseTransport {
77
public sendEvent(_: Event): PromiseLike<Response> {
8-
return this._buffer.add(
8+
return this._buffer.add(() =>
99
SyncPromise.resolve({
1010
status: Status.fromHttpCode(200),
1111
}),

packages/core/test/mocks/transport.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ export class FakeTransport implements Transport {
1616
public sendEvent(_event: Event): PromiseLike<Response> {
1717
this.sendCalled += 1;
1818
return this._buffer.add(
19-
new SyncPromise(async res => {
20-
await sleep(this.delay);
21-
this.sentCount += 1;
22-
res({ status: Status.Success });
23-
}),
19+
() =>
20+
new SyncPromise(async res => {
21+
await sleep(this.delay);
22+
this.sentCount += 1;
23+
res({ status: Status.Success });
24+
}),
2425
);
2526
}
2627

packages/node/src/transports/base/index.ts

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -203,59 +203,62 @@ export abstract class BaseTransport implements Transport {
203203
return Promise.reject(new SentryError('Not adding Promise due to buffer limit reached.'));
204204
}
205205
return this._buffer.add(
206-
new Promise<Response>((resolve, reject) => {
207-
if (!this.module) {
208-
throw new SentryError('No module available');
209-
}
210-
const options = this._getRequestOptions(this.urlParser(sentryRequest.url));
211-
const req = this.module.request(options, res => {
212-
const statusCode = res.statusCode || 500;
213-
const status = Status.fromHttpCode(statusCode);
206+
() =>
207+
new Promise<Response>((resolve, reject) => {
208+
if (!this.module) {
209+
throw new SentryError('No module available');
210+
}
211+
const options = this._getRequestOptions(this.urlParser(sentryRequest.url));
212+
const req = this.module.request(options, res => {
213+
const statusCode = res.statusCode || 500;
214+
const status = Status.fromHttpCode(statusCode);
214215

215-
res.setEncoding('utf8');
216+
res.setEncoding('utf8');
216217

217-
/**
218-
* "Key-value pairs of header names and values. Header names are lower-cased."
219-
* https://nodejs.org/api/http.html#http_message_headers
220-
*/
221-
let retryAfterHeader = res.headers ? res.headers['retry-after'] : '';
222-
retryAfterHeader = (Array.isArray(retryAfterHeader) ? retryAfterHeader[0] : retryAfterHeader) as string;
218+
/**
219+
* "Key-value pairs of header names and values. Header names are lower-cased."
220+
* https://nodejs.org/api/http.html#http_message_headers
221+
*/
222+
let retryAfterHeader = res.headers ? res.headers['retry-after'] : '';
223+
retryAfterHeader = (Array.isArray(retryAfterHeader) ? retryAfterHeader[0] : retryAfterHeader) as string;
223224

224-
let rlHeader = res.headers ? res.headers['x-sentry-rate-limits'] : '';
225-
rlHeader = (Array.isArray(rlHeader) ? rlHeader[0] : rlHeader) as string;
225+
let rlHeader = res.headers ? res.headers['x-sentry-rate-limits'] : '';
226+
rlHeader = (Array.isArray(rlHeader) ? rlHeader[0] : rlHeader) as string;
226227

227-
const headers = {
228-
'x-sentry-rate-limits': rlHeader,
229-
'retry-after': retryAfterHeader,
230-
};
228+
const headers = {
229+
'x-sentry-rate-limits': rlHeader,
230+
'retry-after': retryAfterHeader,
231+
};
231232

232-
const limited = this._handleRateLimit(headers);
233-
if (limited)
234-
logger.warn(
235-
`Too many ${sentryRequest.type} requests, backing off until: ${this._disabledUntil(sentryRequest.type)}`,
236-
);
233+
const limited = this._handleRateLimit(headers);
234+
if (limited)
235+
logger.warn(
236+
`Too many ${sentryRequest.type} requests, backing off until: ${this._disabledUntil(
237+
sentryRequest.type,
238+
)}`,
239+
);
237240

238-
if (status === Status.Success) {
239-
resolve({ status });
240-
} else {
241-
let rejectionMessage = `HTTP Error (${statusCode})`;
242-
if (res.headers && res.headers['x-sentry-error']) {
243-
rejectionMessage += `: ${res.headers['x-sentry-error']}`;
241+
if (status === Status.Success) {
242+
resolve({ status });
243+
} else {
244+
let rejectionMessage = `HTTP Error (${statusCode})`;
245+
if (res.headers && res.headers['x-sentry-error']) {
246+
rejectionMessage += `: ${res.headers['x-sentry-error']}`;
247+
}
248+
reject(new SentryError(rejectionMessage));
244249
}
245-
reject(new SentryError(rejectionMessage));
246-
}
247250

248-
// Force the socket to drain
249-
res.on('data', () => {
250-
// Drain
251+
// Force the socket to drain
252+
res.on('data', () => {
253+
// Drain
254+
});
255+
res.on('end', () => {
256+
// Drain
257+
});
251258
});
252-
res.on('end', () => {
253-
// Drain
254-
});
255-
});
256-
req.on('error', reject);
257-
req.end(sentryRequest.body);
258-
}),
259+
req.on('error', reject);
260+
req.end(sentryRequest.body);
261+
}),
259262
);
260263
}
261264
}

packages/utils/src/promisebuffer.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { SentryError } from './error';
2+
import { isThenable } from './is';
23
import { SyncPromise } from './syncpromise';
34

5+
type TaskProducer<T> = () => PromiseLike<T>;
6+
47
/** A simple queue that holds promises. */
58
export class PromiseBuffer<T> {
69
/** Internal set of queued Promises */
@@ -18,13 +21,22 @@ export class PromiseBuffer<T> {
1821
/**
1922
* Add a promise to the queue.
2023
*
21-
* @param task Can be any PromiseLike<T>
24+
* @param taskProducer A function producing any PromiseLike<T>
2225
* @returns The original promise.
2326
*/
24-
public add(task: PromiseLike<T>): PromiseLike<T> {
27+
public add(taskProducer: PromiseLike<T> | TaskProducer<T>): PromiseLike<T> {
28+
// NOTE: This is necessary to preserve backwards compatibility
29+
// It should accept _only_ `TaskProducer<T>` but we dont want to break other custom transports
30+
// that are utilizing our `Buffer` implementation.
31+
// see: https://github.com/getsentry/sentry-javascript/issues/3725
32+
const normalizedTaskProducer: TaskProducer<T> = isThenable(taskProducer)
33+
? () => taskProducer as PromiseLike<T>
34+
: (taskProducer as TaskProducer<T>);
35+
2536
if (!this.isReady()) {
2637
return SyncPromise.reject(new SentryError('Not adding Promise due to buffer limit reached.'));
2738
}
39+
const task = normalizedTaskProducer();
2840
if (this._buffer.indexOf(task) === -1) {
2941
this._buffer.push(task);
3042
}

packages/utils/test/promisebuffer.test.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,28 @@ describe('PromiseBuffer', () => {
1010
describe('add()', () => {
1111
test('no limit', () => {
1212
const q = new PromiseBuffer<void>();
13-
const p = new SyncPromise<void>(resolve => setTimeout(resolve, 1));
13+
const p = jest.fn(
14+
() => new SyncPromise<void>(resolve => setTimeout(resolve, 1)),
15+
);
1416
q.add(p);
1517
expect(q.length()).toBe(1);
1618
});
19+
1720
test('with limit', () => {
1821
const q = new PromiseBuffer<void>(1);
19-
const p = new SyncPromise<void>(resolve => setTimeout(resolve, 1));
20-
expect(q.add(p)).toEqual(p);
21-
expect(
22-
q.add(
23-
new SyncPromise<void>(resolve => setTimeout(resolve, 1)),
24-
),
25-
).rejects.toThrowError();
22+
let t1;
23+
const p1 = jest.fn(() => {
24+
t1 = new SyncPromise<void>(resolve => setTimeout(resolve, 1));
25+
return t1;
26+
});
27+
const p2 = jest.fn(
28+
() => new SyncPromise<void>(resolve => setTimeout(resolve, 1)),
29+
);
30+
expect(q.add(p1)).toEqual(t1);
31+
expect(q.add(p2)).rejects.toThrowError();
2632
expect(q.length()).toBe(1);
33+
expect(p1).toHaveBeenCalled();
34+
expect(p2).not.toHaveBeenCalled();
2735
});
2836
});
2937

0 commit comments

Comments
 (0)