Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/two-cameras-melt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@team-plain/typescript-sdk': patch
---

Added a new 'requestId' field to all errors for better debugging and support when something unexpected happens.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,32 +6,37 @@ type BadRequestError = {
type: 'bad_request';
message: string;
graphqlErrors: PlainGraphQLError[];
requestId?: string;
};

/* 401 */
type ForbiddenError = {
type: 'forbidden';
message: string;
requestId?: string;
};

/* 500 */
type InternalServerError = {
type: 'internal_server_error';
message: string;
requestId?: string;
};

/* Unhandled/unexpected errors */
type UnknownError = {
type: 'unknown';
message: string;
err?: unknown;
requestId?: string;
};

/* Handled mutation errors */
type MutationError = {
type: 'mutation_error';
message: string;
errorDetails: MutationErrorPartsFragment;
requestId?: string;
};

export type PlainSDKError =
Expand Down
17 changes: 15 additions & 2 deletions src/request.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import type { TypedDocumentNode } from '@graphql-typed-document-node/core';
import type { Result } from './result';
import { print } from 'graphql';
import axios from 'axios';
import axios, { type AxiosResponseHeaders, type RawAxiosResponseHeaders } from 'axios';
import type { Context } from './context';
import type { PlainSDKError } from './error';
import { getMutationErrorFromResponse, isPlainGraphQLResponse } from './graphql-utlities';

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

function getRequestId(headers: AxiosResponseHeaders | RawAxiosResponseHeaders): string | undefined {
const reqId: unknown = headers['apigw-requestid'];

if (reqId && typeof reqId === 'string') {
return reqId;
}
}

export async function request<Query, Variables>(
ctx: Context,
args: {
Expand All @@ -25,7 +33,7 @@ export async function request<Query, Variables>(
const url = ctx.apiUrl || defaultUrl;

// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const { data: res } = await axios.post(
const { data: res, headers: responseHeaders } = await axios.post(
url,
{
query: query,
Expand All @@ -47,6 +55,7 @@ export async function request<Query, Variables>(
error: {
type: 'forbidden',
message: mutationError.message,
requestId: getRequestId(responseHeaders),
},
};
}
Expand All @@ -56,6 +65,7 @@ export async function request<Query, Variables>(
type: 'mutation_error',
message: mutationError.message,
errorDetails: mutationError,
requestId: getRequestId(responseHeaders),
},
};
}
Expand All @@ -72,6 +82,7 @@ export async function request<Query, Variables>(
error: {
type: 'forbidden',
message: 'Authentication failed. Please check the provided API key.',
requestId: getRequestId(err.response.headers),
},
};
}
Expand All @@ -82,6 +93,7 @@ export async function request<Query, Variables>(
type: 'bad_request',
message: 'Missing or invalid arguments provided.',
graphqlErrors: err.response.data.errors || [],
requestId: getRequestId(err.response.headers),
},
};
}
Expand All @@ -91,6 +103,7 @@ export async function request<Query, Variables>(
error: {
type: 'internal_server_error',
message: 'Internal server error.',
requestId: getRequestId(err.response.headers),
},
};
}
Expand Down
44 changes: 30 additions & 14 deletions src/tests/error-handling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('error handling', () => {
});

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

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

Expand All @@ -19,13 +19,18 @@ describe('error handling', () => {
expect(result.error).toEqual({
type: 'internal_server_error',
message: 'Internal server error.',
requestId: 'req_4',
});

scope.done();
});

test('should return a forbidden error when API 401s', async () => {
const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(401, 'unauthorized');
const scope = interceptor
.matchHeader('Authorization', 'Bearer 123')
.reply(401, 'unauthorized', {
'apigw-requestid': 'req_5',
});

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

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

scope.done();
});

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

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

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

scope.done();
});

test('should return a forbidden error when API responds with mutation error', async () => {
const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(200, {
data: {
updateCustomerGroup: {
customerGroup: null,
error: {
__typename: 'MutationError',
message: 'Insufficient permissions, missing "customerGroup:edit".',
type: 'FORBIDDEN',
code: 'forbidden',
fields: [],
const scope = interceptor.matchHeader('Authorization', 'Bearer 123').reply(
200,
{
data: {
updateCustomerGroup: {
customerGroup: null,
error: {
__typename: 'MutationError',
message: 'Insufficient permissions, missing "customerGroup:edit".',
type: 'FORBIDDEN',
code: 'forbidden',
fields: [],
},
},
},
},
});
{
'apigw-requestid': 'req_7',
}
);

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

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

scope.done();
Expand Down
5 changes: 4 additions & 1 deletion src/tests/mutation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ describe('mutation test - create an issue', () => {
},
})
.matchHeader('Authorization', `Bearer 123`)
.reply(200, response);
.reply(200, response, {
'APIGW-REQUESTID': 'req_3',
});

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

expect(result.error).toEqual(err);
Expand Down
13 changes: 10 additions & 3 deletions src/tests/query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,15 @@ describe('query test - customer by id', () => {
variables: {},
})
.matchHeader('Authorization', `Bearer 456`)
.reply(400, {
errors: graphqlErrors,
});
.reply(
400,
{
errors: graphqlErrors,
},
{
'Apigw-Requestid': 'req_1',
}
);

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

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

expect(result.data).toBeUndefined();
Expand Down
13 changes: 10 additions & 3 deletions src/tests/raw-request.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,15 @@ describe('raw request', () => {
variables,
})
.matchHeader('Authorization', `Bearer abc`)
.reply(400, {
errors: graphqlErrors,
});
.reply(
400,
{
errors: graphqlErrors,
},
{
'apigw-requestid': 'req_2',
}
);

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

expect(result.data).toBeUndefined();
Expand Down