Skip to content

Commit be8a61d

Browse files
authored
Add requestId to errors (#30)
* Add requestId to errors * Make typescript gods happy
1 parent 38c92f7 commit be8a61d

File tree

8 files changed

+81
-25
lines changed

8 files changed

+81
-25
lines changed

.changeset/two-cameras-melt.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@team-plain/typescript-sdk': patch
3+
---
4+
5+
Added a new 'requestId' field to all errors for better debugging and support when something unexpected happens.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/error.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,37 @@ type BadRequestError = {
66
type: 'bad_request';
77
message: string;
88
graphqlErrors: PlainGraphQLError[];
9+
requestId?: string;
910
};
1011

1112
/* 401 */
1213
type ForbiddenError = {
1314
type: 'forbidden';
1415
message: string;
16+
requestId?: string;
1517
};
1618

1719
/* 500 */
1820
type InternalServerError = {
1921
type: 'internal_server_error';
2022
message: string;
23+
requestId?: string;
2124
};
2225

2326
/* Unhandled/unexpected errors */
2427
type UnknownError = {
2528
type: 'unknown';
2629
message: string;
2730
err?: unknown;
31+
requestId?: string;
2832
};
2933

3034
/* Handled mutation errors */
3135
type MutationError = {
3236
type: 'mutation_error';
3337
message: string;
3438
errorDetails: MutationErrorPartsFragment;
39+
requestId?: string;
3540
};
3641

3742
export type PlainSDKError =

src/request.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
22
import type { Result } from './result';
33
import { print } from 'graphql';
4-
import axios from 'axios';
4+
import axios, { type AxiosResponseHeaders, type RawAxiosResponseHeaders } from 'axios';
55
import type { Context } from './context';
66
import type { PlainSDKError } from './error';
77
import { getMutationErrorFromResponse, isPlainGraphQLResponse } from './graphql-utlities';
88

99
const defaultUrl = 'https://core-api.uk.plain.com/graphql/v1';
1010

11+
function getRequestId(headers: AxiosResponseHeaders | RawAxiosResponseHeaders): string | undefined {
12+
const reqId: unknown = headers['apigw-requestid'];
13+
14+
if (reqId && typeof reqId === 'string') {
15+
return reqId;
16+
}
17+
}
18+
1119
export async function request<Query, Variables>(
1220
ctx: Context,
1321
args: {
@@ -25,7 +33,7 @@ export async function request<Query, Variables>(
2533
const url = ctx.apiUrl || defaultUrl;
2634

2735
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
28-
const { data: res } = await axios.post(
36+
const { data: res, headers: responseHeaders } = await axios.post(
2937
url,
3038
{
3139
query: query,
@@ -47,6 +55,7 @@ export async function request<Query, Variables>(
4755
error: {
4856
type: 'forbidden',
4957
message: mutationError.message,
58+
requestId: getRequestId(responseHeaders),
5059
},
5160
};
5261
}
@@ -56,6 +65,7 @@ export async function request<Query, Variables>(
5665
type: 'mutation_error',
5766
message: mutationError.message,
5867
errorDetails: mutationError,
68+
requestId: getRequestId(responseHeaders),
5969
},
6070
};
6171
}
@@ -72,6 +82,7 @@ export async function request<Query, Variables>(
7282
error: {
7383
type: 'forbidden',
7484
message: 'Authentication failed. Please check the provided API key.',
85+
requestId: getRequestId(err.response.headers),
7586
},
7687
};
7788
}
@@ -82,6 +93,7 @@ export async function request<Query, Variables>(
8293
type: 'bad_request',
8394
message: 'Missing or invalid arguments provided.',
8495
graphqlErrors: err.response.data.errors || [],
96+
requestId: getRequestId(err.response.headers),
8597
},
8698
};
8799
}
@@ -91,6 +103,7 @@ export async function request<Query, Variables>(
91103
error: {
92104
type: 'internal_server_error',
93105
message: 'Internal server error.',
106+
requestId: getRequestId(err.response.headers),
94107
},
95108
};
96109
}

src/tests/error-handling.test.ts

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ describe('error handling', () => {
1010
});
1111

1212
test('should return an unknown error when something unexpected is returned', async () => {
13-
const scope = interceptor.reply(500, '🌶️');
13+
const scope = interceptor.reply(500, '🌶️', { 'apigw-requestid': 'req_4' });
1414

1515
const client = new PlainClient({ apiKey: '123' });
1616

@@ -19,13 +19,18 @@ describe('error handling', () => {
1919
expect(result.error).toEqual({
2020
type: 'internal_server_error',
2121
message: 'Internal server error.',
22+
requestId: 'req_4',
2223
});
2324

2425
scope.done();
2526
});
2627

2728
test('should return a forbidden error when API 401s', async () => {
28-
const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(401, 'unauthorized');
29+
const scope = interceptor
30+
.matchHeader('Authorization', 'Bearer 123')
31+
.reply(401, 'unauthorized', {
32+
'apigw-requestid': 'req_5',
33+
});
2934

3035
const client = new PlainClient({ apiKey: '123' });
3136

@@ -34,13 +39,16 @@ describe('error handling', () => {
3439
expect(result.error).toEqual({
3540
type: 'forbidden',
3641
message: expect.stringContaining('Authentication failed'),
42+
requestId: 'req_5',
3743
});
3844

3945
scope.done();
4046
});
4147

4248
test('should return a forbidden error when API 403s', async () => {
43-
const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(403, 'forbidden');
49+
const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(403, 'forbidden', {
50+
'apigw-requestid': 'req_6',
51+
});
4452

4553
const client = new PlainClient({ apiKey: '123' });
4654

@@ -49,26 +57,33 @@ describe('error handling', () => {
4957
expect(result.error).toEqual({
5058
type: 'forbidden',
5159
message: expect.stringContaining('Authentication failed'),
60+
requestId: 'req_6',
5261
});
5362

5463
scope.done();
5564
});
5665

5766
test('should return a forbidden error when API responds with mutation error', async () => {
58-
const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(200, {
59-
data: {
60-
updateCustomerGroup: {
61-
customerGroup: null,
62-
error: {
63-
__typename: 'MutationError',
64-
message: 'Insufficient permissions, missing "customerGroup:edit".',
65-
type: 'FORBIDDEN',
66-
code: 'forbidden',
67-
fields: [],
67+
const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(
68+
200,
69+
{
70+
data: {
71+
updateCustomerGroup: {
72+
customerGroup: null,
73+
error: {
74+
__typename: 'MutationError',
75+
message: 'Insufficient permissions, missing "customerGroup:edit".',
76+
type: 'FORBIDDEN',
77+
code: 'forbidden',
78+
fields: [],
79+
},
6880
},
6981
},
7082
},
71-
});
83+
{
84+
'apigw-requestid': 'req_7',
85+
}
86+
);
7287

7388
const client = new PlainClient({ apiKey: '123' });
7489

@@ -77,6 +92,7 @@ describe('error handling', () => {
7792
expect(result.error).toEqual({
7893
type: 'forbidden',
7994
message: 'Insufficient permissions, missing "customerGroup:edit".',
95+
requestId: 'req_7',
8096
});
8197

8298
scope.done();

src/tests/mutation.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ describe('mutation test - create an issue', () => {
112112
},
113113
})
114114
.matchHeader('Authorization', `Bearer 123`)
115-
.reply(200, response);
115+
.reply(200, response, {
116+
'APIGW-REQUESTID': 'req_3',
117+
});
116118

117119
const client = new PlainClient({ apiKey: '123' });
118120
const result = await client.createIssue({ customerId: '', issueTypeId: '', priorityValue: 1 });
@@ -121,6 +123,7 @@ describe('mutation test - create an issue', () => {
121123
type: 'mutation_error',
122124
message: 'There was a validation error.',
123125
errorDetails: graphqlError,
126+
requestId: 'req_3',
124127
};
125128

126129
expect(result.error).toEqual(err);

src/tests/query.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,15 @@ describe('query test - customer by id', () => {
9191
variables: {},
9292
})
9393
.matchHeader('Authorization', `Bearer 456`)
94-
.reply(400, {
95-
errors: graphqlErrors,
96-
});
94+
.reply(
95+
400,
96+
{
97+
errors: graphqlErrors,
98+
},
99+
{
100+
'Apigw-Requestid': 'req_1',
101+
}
102+
);
97103

98104
const client = new PlainClient({ apiKey: '456' });
99105

@@ -105,6 +111,7 @@ describe('query test - customer by id', () => {
105111
type: 'bad_request',
106112
message: 'Missing or invalid arguments provided.',
107113
graphqlErrors,
114+
requestId: 'req_1',
108115
};
109116

110117
expect(result.data).toBeUndefined();

src/tests/raw-request.test.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,15 @@ describe('raw request', () => {
7878
variables,
7979
})
8080
.matchHeader('Authorization', `Bearer abc`)
81-
.reply(400, {
82-
errors: graphqlErrors,
83-
});
81+
.reply(
82+
400,
83+
{
84+
errors: graphqlErrors,
85+
},
86+
{
87+
'apigw-requestid': 'req_2',
88+
}
89+
);
8490

8591
const client = new PlainClient({ apiKey: 'abc' });
8692
const result = await client.rawRequest({
@@ -92,6 +98,7 @@ describe('raw request', () => {
9298
type: 'bad_request',
9399
message: 'Missing or invalid arguments provided.',
94100
graphqlErrors,
101+
requestId: 'req_2',
95102
};
96103

97104
expect(result.data).toBeUndefined();

0 commit comments

Comments
 (0)