-
Notifications
You must be signed in to change notification settings - Fork 953
fix(otlp-exporter-base): ensure retry on network errors during HTTP export #6147
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
29233f3
9c5b5cf
b185034
c3f0aa2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,8 @@ import { IExporterTransport } from '../exporter-transport'; | |
| import { ExportResponse } from '../export-response'; | ||
| import { diag } from '@opentelemetry/api'; | ||
| import { | ||
| isExportRetryable, | ||
| isExportHTTPErrorRetryable, | ||
| isExportNetworkErrorRetryable, | ||
| parseRetryAfterToMills, | ||
| } from '../is-export-retryable'; | ||
| import { HeadersFactory } from '../configuration/otlp-http-configuration'; | ||
|
|
@@ -53,7 +54,7 @@ class FetchTransport implements IExporterTransport { | |
| if (response.status >= 200 && response.status <= 299) { | ||
| diag.debug('response success'); | ||
| return { status: 'success' }; | ||
| } else if (isExportRetryable(response.status)) { | ||
| } else if (isExportHTTPErrorRetryable(response.status)) { | ||
| const retryAfter = response.headers.get('Retry-After'); | ||
| const retryInMillis = parseRetryAfterToMills(retryAfter); | ||
| return { status: 'retryable', retryInMillis }; | ||
|
|
@@ -63,10 +64,10 @@ class FetchTransport implements IExporterTransport { | |
| error: new Error('Fetch request failed with non-retryable status'), | ||
| }; | ||
| } catch (error) { | ||
| if (error?.name === 'AbortError') { | ||
| if (isExportNetworkErrorRetryable(error)) { | ||
| return { | ||
| status: 'failure', | ||
| error: new Error('Fetch request timed out', { cause: error }), | ||
| status: 'retryable', | ||
| retryInMillis: 0, | ||
| }; | ||
| } | ||
|
Comment on lines
67
to
72
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this transport is fully intended for the browser. It's for browser and webworkers. It will therefore never receive any undici errors. |
||
| return { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,8 @@ import * as zlib from 'zlib'; | |
| import { Readable } from 'stream'; | ||
| import { ExportResponse } from '../export-response'; | ||
| import { | ||
| isExportRetryable, | ||
| isExportHTTPErrorRetryable, | ||
| isExportNetworkErrorRetryable, | ||
| parseRetryAfterToMills, | ||
| } from '../is-export-retryable'; | ||
| import { OTLPExporterError } from '../types'; | ||
|
|
@@ -74,7 +75,7 @@ export function sendWithHttp( | |
| status: 'success', | ||
| data: Buffer.concat(responseData), | ||
| }); | ||
| } else if (res.statusCode && isExportRetryable(res.statusCode)) { | ||
| } else if (res.statusCode && isExportHTTPErrorRetryable(res.statusCode)) { | ||
| onDone({ | ||
| status: 'retryable', | ||
| retryInMillis: parseRetryAfterToMills(res.headers['retry-after']), | ||
|
|
@@ -96,16 +97,23 @@ export function sendWithHttp( | |
| req.setTimeout(timeoutMillis, () => { | ||
| req.destroy(); | ||
| onDone({ | ||
| status: 'failure', | ||
| error: new Error('Request Timeout'), | ||
| status: 'retryable', | ||
| retryInMillis: 0, | ||
| }); | ||
| }); | ||
|
|
||
| req.on('error', (error: Error) => { | ||
| onDone({ | ||
| status: 'failure', | ||
| error, | ||
| }); | ||
| if (isExportNetworkErrorRetryable(error)) { | ||
| onDone({ | ||
| status: 'retryable', | ||
| retryInMillis: 0, | ||
| }); | ||
|
Comment on lines
107
to
110
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This changes the behavoir quite significantly:
|
||
| } else { | ||
| onDone({ | ||
| status: 'failure', | ||
| error, | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| compressAndSend(req, compression, data, (error: Error) => { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,7 +18,7 @@ import { IExporterTransport } from '../exporter-transport'; | |
| import { ExportResponse } from '../export-response'; | ||
| import { diag } from '@opentelemetry/api'; | ||
| import { | ||
| isExportRetryable, | ||
| isExportHTTPErrorRetryable, | ||
| parseRetryAfterToMills, | ||
| } from '../is-export-retryable'; | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars | ||
|
|
@@ -49,8 +49,8 @@ class XhrTransport implements IExporterTransport { | |
|
|
||
| xhr.ontimeout = _ => { | ||
| resolve({ | ||
| status: 'failure', | ||
| error: new Error('XHR request timed out'), | ||
| status: 'retryable', | ||
| retryInMillis: 0, | ||
|
||
| }); | ||
| }; | ||
|
|
||
|
|
@@ -60,7 +60,7 @@ class XhrTransport implements IExporterTransport { | |
| resolve({ | ||
| status: 'success', | ||
| }); | ||
| } else if (xhr.status && isExportRetryable(xhr.status)) { | ||
| } else if (xhr.status && isExportHTTPErrorRetryable(xhr.status)) { | ||
| resolve({ | ||
| status: 'retryable', | ||
| retryInMillis: parseRetryAfterToMills( | ||
|
|
@@ -82,9 +82,10 @@ class XhrTransport implements IExporterTransport { | |
| }); | ||
| }; | ||
| xhr.onerror = () => { | ||
| // XHR onerror typically indicates network failures which are retryable | ||
| resolve({ | ||
| status: 'failure', | ||
| error: new Error('XHR request errored'), | ||
| status: 'retryable', | ||
| retryInMillis: 0, | ||
|
||
| }); | ||
| }; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -122,7 +122,7 @@ describe('FetchTransport', function () { | |
| }, done /* catch any rejections */); | ||
| }); | ||
|
|
||
| it('returns failure when request times out', function (done) { | ||
| it('returns failure when request is aborted', function (done) { | ||
| // arrange | ||
| const abortError = new Error('aborted request'); | ||
| abortError.name = 'AbortError'; | ||
|
|
@@ -137,7 +137,7 @@ describe('FetchTransport', function () { | |
| assert.strictEqual(response.status, 'failure'); | ||
| assert.strictEqual( | ||
| (response as ExportResponseFailure).error.message, | ||
| 'Fetch request timed out' | ||
| 'Fetch request errored' | ||
| ); | ||
| } catch (e) { | ||
| done(e); | ||
|
|
@@ -147,7 +147,7 @@ describe('FetchTransport', function () { | |
| clock.tick(requestTimeout + 100); | ||
| }); | ||
|
|
||
| it('returns failure when no server exists', function (done) { | ||
| it('returns failure when fetch throws non-network error', function (done) { | ||
| // arrange | ||
| sinon.stub(globalThis, 'fetch').throws(new Error('fetch failed')); | ||
| const clock = sinon.useFakeTimers(); | ||
|
|
@@ -169,5 +169,51 @@ describe('FetchTransport', function () { | |
| }, done /* catch any rejections */); | ||
| clock.tick(requestTimeout + 100); | ||
| }); | ||
|
|
||
| it('returns retryable when browser fetch throws network error', function (done) { | ||
| // arrange | ||
| // Browser fetch throws TypeError for network errors | ||
| sinon.stub(globalThis, 'fetch').rejects(new TypeError('Failed to fetch')); | ||
| const transport = createFetchTransport(testTransportParameters); | ||
|
|
||
| //act | ||
| transport.send(testPayload, requestTimeout).then(response => { | ||
| // assert | ||
| try { | ||
| assert.strictEqual(response.status, 'retryable'); | ||
| assert.strictEqual( | ||
| (response as ExportResponseRetryable).retryInMillis, | ||
| 0 | ||
| ); | ||
| } catch (e) { | ||
| done(e); | ||
| } | ||
| done(); | ||
| }, done /* catch any rejections */); | ||
| }); | ||
|
|
||
| it('returns retryable when fetch throws network error with code', function (done) { | ||
| // arrange | ||
| const cause = new Error('network error') as NodeJS.ErrnoException; | ||
| cause.code = 'ECONNRESET'; | ||
| const networkError = new TypeError('fetch failed', { cause }); | ||
| sinon.stub(globalThis, 'fetch').rejects(networkError); | ||
| const transport = createFetchTransport(testTransportParameters); | ||
|
|
||
| //act | ||
| transport.send(testPayload, requestTimeout).then(response => { | ||
| // assert | ||
| try { | ||
| assert.strictEqual(response.status, 'retryable'); | ||
| assert.strictEqual( | ||
| (response as ExportResponseRetryable).retryInMillis, | ||
| 0 | ||
| ); | ||
| } catch (e) { | ||
| done(e); | ||
| } | ||
| done(); | ||
| }, done /* catch any rejections */); | ||
| }); | ||
| }); | ||
|
||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think there's any code path right now that would produce undici errors.