Skip to content

Commit 608cebf

Browse files
authored
Better JSON messages (#8379)
1 parent 92caa93 commit 608cebf

File tree

8 files changed

+98
-16
lines changed

8 files changed

+98
-16
lines changed

packages/data-connect/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
## UNRELEASED
33
* Updated reporting to use @firebase/data-connect instead of @firebase/connect
44
* Added functionality to retry queries and mutations if the server responds with UNAUTHENTICATED.
5-
5+
* Updated errors to only show relevant details to the user.

packages/data-connect/src/network/fetch.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,15 @@ export function dcFetch<T, U>(
6464
} catch (e) {
6565
throw new DataConnectError(Code.OTHER, JSON.stringify(e));
6666
}
67+
const message = getMessage(jsonResponse);
6768
if (response.status >= 400) {
6869
logError(
6970
'Error while performing request: ' + JSON.stringify(jsonResponse)
7071
);
7172
if (response.status === 401) {
72-
throw new DataConnectError(
73-
Code.UNAUTHORIZED,
74-
JSON.stringify(jsonResponse)
75-
);
73+
throw new DataConnectError(Code.UNAUTHORIZED, message);
7674
}
77-
throw new DataConnectError(Code.OTHER, JSON.stringify(jsonResponse));
75+
throw new DataConnectError(Code.OTHER, message);
7876
}
7977
return jsonResponse;
8078
})
@@ -87,3 +85,12 @@ export function dcFetch<T, U>(
8785
return res as { data: T; errors: Error[] };
8886
});
8987
}
88+
interface MessageObject {
89+
message?: string;
90+
}
91+
function getMessage(obj: MessageObject): string {
92+
if ('message' in obj) {
93+
return obj.message;
94+
}
95+
return JSON.stringify(obj);
96+
}

packages/data-connect/test/emulatorSeeder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export async function setupQueries(
7878
})
7979
}
8080
},
81-
// eslint-disable-line camelcase
81+
// eslint-disable-next-line camelcase
8282
connection_string:
8383
'postgresql://postgres:secretpassword@localhost:5432/postgres?sslmode=disable'
8484
};

packages/data-connect/test/post.gql

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,10 @@ query listPosts @auth(level: PUBLIC) {
99
content
1010
}
1111
}
12+
query listPosts2 {
13+
posts {
14+
id,
15+
content
16+
}
17+
}
1218

packages/data-connect/test/queries.test.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ const SEEDED_DATA = [
5959
content: 'task 2'
6060
}
6161
];
62+
const REAL_DATA = SEEDED_DATA.map(obj => ({
63+
...obj,
64+
id: obj.id.replace(/-/g, '')
65+
}));
6266
function seedDatabase(instance: DataConnect): Promise<void> {
6367
// call mutation query that adds SEEDED_DATA to database
6468
return new Promise((resolve, reject) => {
@@ -100,7 +104,7 @@ describe('DataConnect Tests', async () => {
100104
const taskListQuery = queryRef<TaskListResponse>(dc, 'listPosts');
101105
const taskListRes = await executeQuery(taskListQuery);
102106
expect(taskListRes.data).to.deep.eq({
103-
posts: SEEDED_DATA
107+
posts: REAL_DATA
104108
});
105109
});
106110
it(`instantly executes a query if one hasn't been subscribed to`, async () => {
@@ -121,7 +125,7 @@ describe('DataConnect Tests', async () => {
121125
);
122126
const res = await promise;
123127
expect(res.data).to.deep.eq({
124-
posts: SEEDED_DATA
128+
posts: REAL_DATA
125129
});
126130
expect(res.source).to.eq(SOURCE_SERVER);
127131
});
@@ -138,7 +142,7 @@ describe('DataConnect Tests', async () => {
138142
const result = await waitForFirstEvent(taskListQuery);
139143
const serializedRef: SerializedRef<TaskListResponse, undefined> = {
140144
data: {
141-
posts: SEEDED_DATA
145+
posts: REAL_DATA
142146
},
143147
fetchTime: Date.now().toLocaleString(),
144148
refInfo: {
@@ -149,7 +153,7 @@ describe('DataConnect Tests', async () => {
149153
name: taskListQuery.name,
150154
variables: undefined
151155
},
152-
source: SOURCE_SERVER
156+
source: SOURCE_CACHE
153157
};
154158
expect(result.toJSON()).to.deep.eq(serializedRef);
155159
expect(result.source).to.deep.eq(SOURCE_CACHE);
@@ -167,6 +171,14 @@ describe('DataConnect Tests', async () => {
167171
'ECONNREFUSED'
168172
);
169173
});
174+
it('throws an error with just the message when the server responds with an error', async () => {
175+
const invalidTaskListQuery = queryRef<TaskListResponse>(dc, 'listPosts2');
176+
const message =
177+
'unauthorized: you are not authorized to perform this operation';
178+
await expect(
179+
executeQuery(invalidTaskListQuery)
180+
).to.eventually.be.rejectedWith(message);
181+
});
170182
});
171183
async function waitForFirstEvent<Data, Variables>(
172184
query: QueryRef<Data, Variables>

packages/data-connect/test/unit/dataconnect.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ import { ConnectorConfig, getDataConnect } from '../../src';
2222

2323
describe('Data Connect Test', () => {
2424
it('should throw an error if `projectId` is not provided', async () => {
25-
const app = initializeApp({});
25+
const app = initializeApp({ projectId: undefined }, 'a');
2626
expect(() =>
27-
getDataConnect({ connector: 'c', location: 'l', service: 's' })
27+
getDataConnect(app, { connector: 'c', location: 'l', service: 's' })
2828
).to.throw(
2929
'Project ID must be provided. Did you pass in a proper projectId to initializeApp?'
3030
);
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/**
2+
* @license
3+
* Copyright 2024 Google LLC
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import { expect, use } from 'chai';
19+
import chaiAsPromised from 'chai-as-promised';
20+
import * as sinon from 'sinon';
21+
22+
import { dcFetch, initializeFetch } from '../../src/network/fetch';
23+
use(chaiAsPromised);
24+
function mockFetch(json: object): void {
25+
const fakeFetchImpl = sinon.stub().returns(
26+
Promise.resolve({
27+
json: () => {
28+
return Promise.resolve(json);
29+
},
30+
status: 401
31+
} as Response)
32+
);
33+
initializeFetch(fakeFetchImpl);
34+
}
35+
describe('fetch', () => {
36+
it('should throw an error with just the message when the server responds with an error with a message property in the body', async () => {
37+
const message = 'Failed to connect to Postgres instance';
38+
mockFetch({
39+
code: 401,
40+
message
41+
});
42+
await expect(
43+
dcFetch('http://localhost', {}, {} as AbortController, null)
44+
).to.eventually.be.rejectedWith(message);
45+
});
46+
it('should throw a stringified message when the server responds with an error without a message property in the body', async () => {
47+
const message = 'Failed to connect to Postgres instance';
48+
const json = {
49+
code: 401,
50+
message1: message
51+
};
52+
mockFetch(json);
53+
await expect(
54+
dcFetch('http://localhost', {}, {} as AbortController, null)
55+
).to.eventually.be.rejectedWith(JSON.stringify(json));
56+
});
57+
});

packages/data-connect/test/unit/queries.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('Queries', () => {
7070
const authProvider = new FakeAuthProvider();
7171
const rt = new RESTTransport(options, undefined, authProvider);
7272
await expect(rt.invokeQuery('test', null)).to.eventually.be.rejectedWith(
73-
JSON.stringify(json)
73+
json.message
7474
);
7575
expect(fakeFetchImpl.callCount).to.eq(2);
7676
});
@@ -79,7 +79,7 @@ describe('Queries', () => {
7979
const authProvider = new FakeAuthProvider();
8080
const rt = new RESTTransport(options, undefined, authProvider);
8181
await expect(rt.invokeMutation('test', null)).to.eventually.be.rejectedWith(
82-
JSON.stringify(json)
82+
json.message
8383
);
8484
expect(fakeFetchImpl.callCount).to.eq(2);
8585
});
@@ -90,7 +90,7 @@ describe('Queries', () => {
9090
rt._setLastToken('initial token');
9191
await expect(
9292
rt.invokeQuery('test', null) as Promise<unknown>
93-
).to.eventually.be.rejectedWith(JSON.stringify(json));
93+
).to.eventually.be.rejectedWith(json.message);
9494
expect(fakeFetchImpl.callCount).to.eq(1);
9595
});
9696
});

0 commit comments

Comments
 (0)