From d9de4a12a2f55e1720577fa994e319703a91d265 Mon Sep 17 00:00:00 2001 From: Mathias Vagni Date: Wed, 31 May 2023 10:07:43 +0200 Subject: [PATCH 1/2] Add requestId to errors --- .changeset/two-cameras-melt.md | 5 ++++ src/error.ts | 5 ++++ src/request.ts | 13 ++++++++-- src/tests/error-handling.test.ts | 44 ++++++++++++++++++++++---------- src/tests/mutation.test.ts | 5 +++- src/tests/query.test.ts | 13 +++++++--- src/tests/raw-request.test.ts | 13 +++++++--- 7 files changed, 75 insertions(+), 23 deletions(-) create mode 100644 .changeset/two-cameras-melt.md diff --git a/.changeset/two-cameras-melt.md b/.changeset/two-cameras-melt.md new file mode 100644 index 0000000..e224b00 --- /dev/null +++ b/.changeset/two-cameras-melt.md @@ -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. diff --git a/src/error.ts b/src/error.ts index 560b0b9..8ed6584 100644 --- a/src/error.ts +++ b/src/error.ts @@ -6,18 +6,21 @@ 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 */ @@ -25,6 +28,7 @@ type UnknownError = { type: 'unknown'; message: string; err?: unknown; + requestId?: string; }; /* Handled mutation errors */ @@ -32,6 +36,7 @@ type MutationError = { type: 'mutation_error'; message: string; errorDetails: MutationErrorPartsFragment; + requestId?: string; }; export type PlainSDKError = diff --git a/src/request.ts b/src/request.ts index 1da34e4..4dfa826 100644 --- a/src/request.ts +++ b/src/request.ts @@ -1,13 +1,17 @@ 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 { + return headers['apigw-requestid']; +} + export async function request( ctx: Context, args: { @@ -25,7 +29,7 @@ export async function request( 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, @@ -47,6 +51,7 @@ export async function request( error: { type: 'forbidden', message: mutationError.message, + requestId: getRequestId(responseHeaders), }, }; } @@ -56,6 +61,7 @@ export async function request( type: 'mutation_error', message: mutationError.message, errorDetails: mutationError, + requestId: getRequestId(responseHeaders), }, }; } @@ -72,6 +78,7 @@ export async function request( error: { type: 'forbidden', message: 'Authentication failed. Please check the provided API key.', + requestId: getRequestId(err.response.headers), }, }; } @@ -82,6 +89,7 @@ export async function request( type: 'bad_request', message: 'Missing or invalid arguments provided.', graphqlErrors: err.response.data.errors || [], + requestId: getRequestId(err.response.headers), }, }; } @@ -91,6 +99,7 @@ export async function request( error: { type: 'internal_server_error', message: 'Internal server error.', + requestId: getRequestId(err.response.headers), }, }; } diff --git a/src/tests/error-handling.test.ts b/src/tests/error-handling.test.ts index 2677e16..ca137a0 100644 --- a/src/tests/error-handling.test.ts +++ b/src/tests/error-handling.test.ts @@ -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' }); @@ -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' }); @@ -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' }); @@ -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' }); @@ -77,6 +92,7 @@ describe('error handling', () => { expect(result.error).toEqual({ type: 'forbidden', message: 'Insufficient permissions, missing "customerGroup:edit".', + requestId: 'req_7', }); scope.done(); diff --git a/src/tests/mutation.test.ts b/src/tests/mutation.test.ts index a557e90..c8bc6b1 100644 --- a/src/tests/mutation.test.ts +++ b/src/tests/mutation.test.ts @@ -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 }); @@ -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); diff --git a/src/tests/query.test.ts b/src/tests/query.test.ts index 931882d..b9fe65a 100644 --- a/src/tests/query.test.ts +++ b/src/tests/query.test.ts @@ -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' }); @@ -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(); diff --git a/src/tests/raw-request.test.ts b/src/tests/raw-request.test.ts index 82a28eb..778a6fb 100644 --- a/src/tests/raw-request.test.ts +++ b/src/tests/raw-request.test.ts @@ -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({ @@ -92,6 +98,7 @@ describe('raw request', () => { type: 'bad_request', message: 'Missing or invalid arguments provided.', graphqlErrors, + requestId: 'req_2', }; expect(result.data).toBeUndefined(); From 4c8cc44f35eb9035f0b7294c29c9845675dae684 Mon Sep 17 00:00:00 2001 From: Mathias Vagni Date: Wed, 31 May 2023 10:18:41 +0200 Subject: [PATCH 2/2] Make typescript gods happy --- package-lock.json | 4 ++-- src/request.ts | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b6df70d..4f9c6f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@team-plain/typescript-sdk", - "version": "0.0.7", + "version": "1.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@team-plain/typescript-sdk", - "version": "0.0.7", + "version": "1.2.0", "license": "MIT", "dependencies": { "@graphql-typed-document-node/core": "^3.2.0", diff --git a/src/request.ts b/src/request.ts index 4dfa826..c895f17 100644 --- a/src/request.ts +++ b/src/request.ts @@ -9,7 +9,11 @@ import { getMutationErrorFromResponse, isPlainGraphQLResponse } from './graphql- const defaultUrl = 'https://core-api.uk.plain.com/graphql/v1'; function getRequestId(headers: AxiosResponseHeaders | RawAxiosResponseHeaders): string | undefined { - return headers['apigw-requestid']; + const reqId: unknown = headers['apigw-requestid']; + + if (reqId && typeof reqId === 'string') { + return reqId; + } } export async function request(