From 135379e53706911e8651a935bcf132e24f17023e Mon Sep 17 00:00:00 2001 From: lwin Date: Mon, 5 Jan 2026 23:59:30 +0800 Subject: [PATCH 1/7] feat: captureException with claims messenger --- .../src/ClaimsService.test.ts | 19 ++++++++-- .../claims-controller/src/ClaimsService.ts | 22 ++++++++++++ packages/claims-controller/src/utils.test.ts | 35 +++++++++++++++++++ packages/claims-controller/src/utils.ts | 32 +++++++++++++++++ .../tests/mocks/messenger.ts | 3 ++ 5 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 packages/claims-controller/src/utils.test.ts create mode 100644 packages/claims-controller/src/utils.ts diff --git a/packages/claims-controller/src/ClaimsService.test.ts b/packages/claims-controller/src/ClaimsService.test.ts index ee8b7605207..eb5112a7a83 100644 --- a/packages/claims-controller/src/ClaimsService.test.ts +++ b/packages/claims-controller/src/ClaimsService.test.ts @@ -10,20 +10,22 @@ import type { ClaimsConfigurationsResponse, GenerateSignatureMessageResponse, } from './types'; +import { createSentryError } from './utils'; import { createMockClaimsServiceMessenger } from '../tests/mocks/messenger'; const mockAuthenticationControllerGetBearerToken = jest.fn(); const mockFetchFunction = jest.fn(); - +const mockCaptureException = jest.fn(); /** * Create a mock claims service. * * @param env - The environment to use for the mock claims service. Defaults to Env.DEV. - * @returns A mock claims service. + * @returns A mock claims service and its messenger. */ function createMockClaimsService(env: Env = Env.DEV): ClaimsService { const { messenger } = createMockClaimsServiceMessenger( mockAuthenticationControllerGetBearerToken, + mockCaptureException, ); return new ClaimsService({ env, @@ -68,7 +70,10 @@ describe('ClaimsService', () => { }); it('should create instance with valid config', () => { - const { messenger } = createMockClaimsServiceMessenger(jest.fn()); + const { messenger } = createMockClaimsServiceMessenger( + jest.fn(), + jest.fn(), + ); const service = new ClaimsService({ env: Env.DEV, messenger, @@ -289,6 +294,7 @@ describe('ClaimsService', () => { mockFetchFunction.mockResolvedValueOnce({ ok: false, + status: 500, json: jest.fn().mockResolvedValueOnce(null), }); @@ -299,6 +305,13 @@ describe('ClaimsService', () => { ).rejects.toThrow( ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, ); + + expect(mockCaptureException).toHaveBeenCalledWith( + createSentryError( + ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, + new Error('HTTP 500 error'), + ), + ); }); }); }); diff --git a/packages/claims-controller/src/ClaimsService.ts b/packages/claims-controller/src/ClaimsService.ts index f73d6d1c5e5..f42a009765c 100644 --- a/packages/claims-controller/src/ClaimsService.ts +++ b/packages/claims-controller/src/ClaimsService.ts @@ -13,6 +13,7 @@ import type { ClaimsConfigurationsResponse, GenerateSignatureMessageResponse, } from './types'; +import { createSentryError, getErrorFromResponse } from './utils'; export type ClaimsServiceFetchClaimsConfigurationsAction = { type: `${typeof SERVICE_NAME}:fetchClaimsConfigurations`; @@ -143,6 +144,13 @@ export class ClaimsService { }); if (!response.ok) { + const error = await getErrorFromResponse(response); + this.#messenger.captureException?.( + createSentryError( + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS, + error, + ), + ); throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS); } @@ -164,6 +172,13 @@ export class ClaimsService { }); if (!response.ok) { + const error = await getErrorFromResponse(response); + this.#messenger.captureException?.( + createSentryError( + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, + error, + ), + ); throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID); } @@ -197,6 +212,13 @@ export class ClaimsService { }); if (!response.ok) { + const error = await getErrorFromResponse(response); + this.#messenger.captureException?.( + createSentryError( + ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, + error, + ), + ); throw new Error( ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, ); diff --git a/packages/claims-controller/src/utils.test.ts b/packages/claims-controller/src/utils.test.ts new file mode 100644 index 00000000000..69d9fa29e2e --- /dev/null +++ b/packages/claims-controller/src/utils.test.ts @@ -0,0 +1,35 @@ +import { getErrorFromResponse, createSentryError } from './utils'; + +describe('getErrorFromResponse', () => { + it('returns error with message from JSON response', async () => { + const response = { + status: 400, + json: jest.fn().mockResolvedValue({ error: 'Bad request' }), + } as unknown as Response; + + const error = await getErrorFromResponse(response); + + expect(error.message).toBe('error: Bad request, statusCode: 400'); + }); + + it('returns generic HTTP error when JSON parsing fails', async () => { + const response = { + status: 500, + json: jest.fn().mockRejectedValue(new Error('Invalid JSON')), + } as unknown as Response; + + const error = await getErrorFromResponse(response); + + expect(error.message).toBe('HTTP 500 error'); + }); +}); + +describe('createSentryError', () => { + it('creates error with message and cause', () => { + const cause = new Error('Original error'); + const error = createSentryError('Something went wrong', cause); + + expect(error.message).toBe('Something went wrong'); + expect((error as Error & { cause: Error }).cause).toBe(cause); + }); +}); diff --git a/packages/claims-controller/src/utils.ts b/packages/claims-controller/src/utils.ts new file mode 100644 index 00000000000..982b2272fba --- /dev/null +++ b/packages/claims-controller/src/utils.ts @@ -0,0 +1,32 @@ +/** + * Get an error from a response. + * + * @param response - The response to get an error from. + * @returns An error. + */ +export async function getErrorFromResponse(response: Response): Promise { + const statusCode = response.status; + try { + const json = await response.json(); + const message = `error: ${json.error}, statusCode: ${statusCode}`; + return new Error(message); + } catch { + return new Error(`HTTP ${statusCode} error`); + } +} + +/** + * Creates an error instance with a readable message and the root cause. + * + * @param message - The error message to create a Sentry error from. + * @param cause - The inner error to create a Sentry error from. + * @returns A Sentry error. + */ +export function createSentryError(message: string, cause: Error): Error { + const sentryError = new Error(message) as Error & { + cause: Error; + }; + sentryError.cause = cause; + + return sentryError; +} diff --git a/packages/claims-controller/tests/mocks/messenger.ts b/packages/claims-controller/tests/mocks/messenger.ts index 4ccae3ac5a5..7836f5b52c1 100644 --- a/packages/claims-controller/tests/mocks/messenger.ts +++ b/packages/claims-controller/tests/mocks/messenger.ts @@ -124,10 +124,12 @@ export type RootServiceMessenger = Messenger< * Create a mock messenger for the claims service. * * @param mockAuthenticationControllerGetBearerToken - A mock function for the authentication controller get bearer token. + * @param mockCaptureException - A mock function for the capture exception. * @returns A mock messenger for the claims service. */ export function createMockClaimsServiceMessenger( mockAuthenticationControllerGetBearerToken: jest.Mock, + mockCaptureException: jest.Mock, ): { rootMessenger: RootServiceMessenger; messenger: ClaimsServiceMessenger; @@ -149,6 +151,7 @@ export function createMockClaimsServiceMessenger( >({ namespace: SERVICE_NAME, parent: rootMessenger, + captureException: mockCaptureException, }); rootMessenger.delegate({ From 2eac569df05ea563506987f4159958a5c25bf306 Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 6 Jan 2026 00:03:05 +0800 Subject: [PATCH 2/7] chore: updated ChangeLog --- packages/claims-controller/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/claims-controller/CHANGELOG.md b/packages/claims-controller/CHANGELOG.md index 1502ab0a34a..a420c5adda0 100644 --- a/packages/claims-controller/CHANGELOG.md +++ b/packages/claims-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Capture claims error and report to sentry using `Messenger.captureException` method from `@metamask/messenger`. ([#7553](https://github.com/MetaMask/core/pull/7553)) + ## [0.3.1] ### Added From c9e08b52666fe81aaf4df4a08f14cae06e7bce5d Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 6 Jan 2026 00:57:28 +0800 Subject: [PATCH 3/7] fix: handle fetch callback errors --- .../src/ClaimsService.test.ts | 19 +++ .../claims-controller/src/ClaimsService.ts | 156 ++++++++++++------ 2 files changed, 121 insertions(+), 54 deletions(-) diff --git a/packages/claims-controller/src/ClaimsService.test.ts b/packages/claims-controller/src/ClaimsService.test.ts index eb5112a7a83..95bef6b266d 100644 --- a/packages/claims-controller/src/ClaimsService.test.ts +++ b/packages/claims-controller/src/ClaimsService.test.ts @@ -236,6 +236,25 @@ describe('ClaimsService', () => { ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, ); }); + + it('should handle fetch error and capture exception', async () => { + mockFetchFunction.mockRestore(); + + mockFetchFunction.mockRejectedValueOnce(new Error('Fetch error')); + + const service = createMockClaimsService(); + + await expect(service.getClaimById('1')).rejects.toThrow( + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, + ); + + expect(mockCaptureException).toHaveBeenCalledWith( + createSentryError( + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, + new Error('Fetch error'), + ), + ); + }); }); describe('generateMessageForClaimSignature', () => { diff --git a/packages/claims-controller/src/ClaimsService.ts b/packages/claims-controller/src/ClaimsService.ts index f42a009765c..641123e8883 100644 --- a/packages/claims-controller/src/ClaimsService.ts +++ b/packages/claims-controller/src/ClaimsService.ts @@ -115,20 +115,33 @@ export class ClaimsService { * @returns The required configurations for the claims service. */ async fetchClaimsConfigurations(): Promise { - const headers = await this.getRequestHeaders(); - const url = `${this.getClaimsApiUrl()}/configurations`; - const response = await this.#fetch(url, { - headers, - }); - - if (!response.ok) { + try { + const headers = await this.getRequestHeaders(); + const url = `${this.getClaimsApiUrl()}/configurations`; + const response = await this.#fetch(url, { + headers, + }); + + if (!response.ok) { + throw new Error( + ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, + ); + } + + const configurations = await response.json(); + return configurations; + } catch (error) { + console.error('fetchClaimsConfigurations', error); + this.#messenger.captureException?.( + createSentryError( + ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, + error as Error, + ), + ); throw new Error( ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, ); } - - const configurations = await response.json(); - return configurations; } /** @@ -137,25 +150,36 @@ export class ClaimsService { * @returns The claims for the current user. */ async getClaims(): Promise { - const headers = await this.getRequestHeaders(); - const url = `${this.getClaimsApiUrl()}/claims`; - const response = await this.#fetch(url, { - headers, - }); - - if (!response.ok) { - const error = await getErrorFromResponse(response); + try { + const headers = await this.getRequestHeaders(); + const url = `${this.getClaimsApiUrl()}/claims`; + const response = await this.#fetch(url, { + headers, + }); + + if (!response.ok) { + const error = await getErrorFromResponse(response); + this.#messenger.captureException?.( + createSentryError( + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS, + error, + ), + ); + throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS); + } + + const claims = await response.json(); + return claims; + } catch (error) { + console.error('getClaims', error); this.#messenger.captureException?.( createSentryError( ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS, - error, + error as Error, ), ); throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS); } - - const claims = await response.json(); - return claims; } /** @@ -165,25 +189,36 @@ export class ClaimsService { * @returns The claim by id. */ async getClaimById(id: string): Promise { - const headers = await this.getRequestHeaders(); - const url = `${this.getClaimsApiUrl()}/claims/byId/${id}`; - const response = await this.#fetch(url, { - headers, - }); - - if (!response.ok) { - const error = await getErrorFromResponse(response); + try { + const headers = await this.getRequestHeaders(); + const url = `${this.getClaimsApiUrl()}/claims/byId/${id}`; + const response = await this.#fetch(url, { + headers, + }); + + if (!response.ok) { + const error = await getErrorFromResponse(response); + this.#messenger.captureException?.( + createSentryError( + ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, + error, + ), + ); + throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID); + } + + const claim = await response.json(); + return claim; + } catch (error) { + console.error('getClaimById', error); this.#messenger.captureException?.( createSentryError( ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, - error, + error as Error, ), ); throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID); } - - const claim = await response.json(); - return claim; } /** @@ -197,35 +232,48 @@ export class ClaimsService { chainId: number, walletAddress: Hex, ): Promise { - const headers = await this.getRequestHeaders(); - const url = `${this.getClaimsApiUrl()}/signature/generateMessage`; - const response = await this.#fetch(url, { - method: 'POST', - headers: { - ...headers, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - chainId, - walletAddress, - }), - }); - - if (!response.ok) { - const error = await getErrorFromResponse(response); + try { + const headers = await this.getRequestHeaders(); + const url = `${this.getClaimsApiUrl()}/signature/generateMessage`; + const response = await this.#fetch(url, { + method: 'POST', + headers: { + ...headers, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + chainId, + walletAddress, + }), + }); + + if (!response.ok) { + const error = await getErrorFromResponse(response); + this.#messenger.captureException?.( + createSentryError( + ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, + error, + ), + ); + throw new Error( + ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, + ); + } + + const message = await response.json(); + return message; + } catch (error) { + console.error('generateMessageForClaimSignature', error); this.#messenger.captureException?.( createSentryError( ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, - error, + error as Error, ), ); throw new Error( ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, ); } - - const message = await response.json(); - return message; } /** From 411a9d73260ddf1c7ed01b757a27637be4076b92 Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 6 Jan 2026 01:29:25 +0800 Subject: [PATCH 4/7] fix: duplicate sentry capture --- .../claims-controller/src/ClaimsService.ts | 31 +++---------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/packages/claims-controller/src/ClaimsService.ts b/packages/claims-controller/src/ClaimsService.ts index 641123e8883..4b8d32e20ad 100644 --- a/packages/claims-controller/src/ClaimsService.ts +++ b/packages/claims-controller/src/ClaimsService.ts @@ -123,9 +123,8 @@ export class ClaimsService { }); if (!response.ok) { - throw new Error( - ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS, - ); + const error = await getErrorFromResponse(response); + throw error; } const configurations = await response.json(); @@ -159,13 +158,7 @@ export class ClaimsService { if (!response.ok) { const error = await getErrorFromResponse(response); - this.#messenger.captureException?.( - createSentryError( - ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS, - error, - ), - ); - throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS); + throw error; } const claims = await response.json(); @@ -198,13 +191,7 @@ export class ClaimsService { if (!response.ok) { const error = await getErrorFromResponse(response); - this.#messenger.captureException?.( - createSentryError( - ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID, - error, - ), - ); - throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID); + throw error; } const claim = await response.json(); @@ -249,15 +236,7 @@ export class ClaimsService { if (!response.ok) { const error = await getErrorFromResponse(response); - this.#messenger.captureException?.( - createSentryError( - ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, - error, - ), - ); - throw new Error( - ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, - ); + throw error; } const message = await response.json(); From 9feba82492c48f9b30a26adfeb8828fd96cca307 Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 6 Jan 2026 09:47:49 +0800 Subject: [PATCH 5/7] chore: updated error parsing and utils --- packages/claims-controller/src/utils.test.ts | 20 ++++++++++++++++++++ packages/claims-controller/src/utils.ts | 5 +++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/claims-controller/src/utils.test.ts b/packages/claims-controller/src/utils.test.ts index 69d9fa29e2e..e28d75d1bed 100644 --- a/packages/claims-controller/src/utils.test.ts +++ b/packages/claims-controller/src/utils.test.ts @@ -12,6 +12,26 @@ describe('getErrorFromResponse', () => { expect(error.message).toBe('error: Bad request, statusCode: 400'); }); + it('returns error with message from JSON response when message is present', async () => { + const response = { + status: 400, + json: jest.fn().mockResolvedValue({ message: 'Bad request' }), + } as unknown as Response; + + const error = await getErrorFromResponse(response); + expect(error.message).toBe('error: Bad request, statusCode: 400'); + }); + + it('returns unknown error when JSON response has no error or message', async () => { + const response = { + status: 400, + json: jest.fn().mockResolvedValue({}), + } as unknown as Response; + + const error = await getErrorFromResponse(response); + expect(error.message).toBe('error: Unknown error, statusCode: 400'); + }); + it('returns generic HTTP error when JSON parsing fails', async () => { const response = { status: 500, diff --git a/packages/claims-controller/src/utils.ts b/packages/claims-controller/src/utils.ts index 982b2272fba..da6bc42c6d9 100644 --- a/packages/claims-controller/src/utils.ts +++ b/packages/claims-controller/src/utils.ts @@ -8,8 +8,9 @@ export async function getErrorFromResponse(response: Response): Promise { const statusCode = response.status; try { const json = await response.json(); - const message = `error: ${json.error}, statusCode: ${statusCode}`; - return new Error(message); + const errorMessage = json?.error ?? json?.message ?? 'Unknown error'; + const networkError = `error: ${errorMessage}, statusCode: ${statusCode}`; + return new Error(networkError); } catch { return new Error(`HTTP ${statusCode} error`); } From ba7a9c7bba68139941feb179389b9040436933de Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 6 Jan 2026 10:00:37 +0800 Subject: [PATCH 6/7] chore: fixed tests --- packages/claims-controller/src/ClaimsService.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/claims-controller/src/ClaimsService.test.ts b/packages/claims-controller/src/ClaimsService.test.ts index 95bef6b266d..ee137925fed 100644 --- a/packages/claims-controller/src/ClaimsService.test.ts +++ b/packages/claims-controller/src/ClaimsService.test.ts @@ -328,7 +328,7 @@ describe('ClaimsService', () => { expect(mockCaptureException).toHaveBeenCalledWith( createSentryError( ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED, - new Error('HTTP 500 error'), + new Error('error: Unknown error, statusCode: 500'), ), ); }); From cd6eae6b06a3ae081b31b1733351e6a6f935551a Mon Sep 17 00:00:00 2001 From: lwin Date: Tue, 6 Jan 2026 12:59:49 +0800 Subject: [PATCH 7/7] feat: updated getErrorFromResponse utils to handle different content-type --- packages/claims-controller/src/utils.test.ts | 64 ++++++++++++++++++++ packages/claims-controller/src/utils.ts | 20 +++++- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/packages/claims-controller/src/utils.test.ts b/packages/claims-controller/src/utils.test.ts index e28d75d1bed..7b4a58c363f 100644 --- a/packages/claims-controller/src/utils.test.ts +++ b/packages/claims-controller/src/utils.test.ts @@ -4,6 +4,9 @@ describe('getErrorFromResponse', () => { it('returns error with message from JSON response', async () => { const response = { status: 400, + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, json: jest.fn().mockResolvedValue({ error: 'Bad request' }), } as unknown as Response; @@ -15,6 +18,9 @@ describe('getErrorFromResponse', () => { it('returns error with message from JSON response when message is present', async () => { const response = { status: 400, + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, json: jest.fn().mockResolvedValue({ message: 'Bad request' }), } as unknown as Response; @@ -25,6 +31,9 @@ describe('getErrorFromResponse', () => { it('returns unknown error when JSON response has no error or message', async () => { const response = { status: 400, + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, json: jest.fn().mockResolvedValue({}), } as unknown as Response; @@ -32,9 +41,50 @@ describe('getErrorFromResponse', () => { expect(error.message).toBe('error: Unknown error, statusCode: 400'); }); + it('returns error with message from text/plain response', async () => { + const response = { + status: 400, + headers: { + get: jest.fn().mockReturnValue('text/plain'), + }, + text: jest.fn().mockResolvedValue('Plain text error'), + } as unknown as Response; + + const error = await getErrorFromResponse(response); + expect(error.message).toBe('error: Plain text error, statusCode: 400'); + }); + + it('returns error with data property when content-type is unknown', async () => { + const response = { + status: 400, + headers: { + get: jest.fn().mockReturnValue('application/octet-stream'), + }, + data: 'Some data error', + } as unknown as Response; + + const error = await getErrorFromResponse(response); + expect(error.message).toBe('error: Some data error, statusCode: 400'); + }); + + it('returns unknown error when content-type is unknown and no data property', async () => { + const response = { + status: 400, + headers: { + get: jest.fn().mockReturnValue('application/octet-stream'), + }, + } as unknown as Response; + + const error = await getErrorFromResponse(response); + expect(error.message).toBe('error: Unknown error, statusCode: 400'); + }); + it('returns generic HTTP error when JSON parsing fails', async () => { const response = { status: 500, + headers: { + get: jest.fn().mockReturnValue('application/json'), + }, json: jest.fn().mockRejectedValue(new Error('Invalid JSON')), } as unknown as Response; @@ -42,6 +92,20 @@ describe('getErrorFromResponse', () => { expect(error.message).toBe('HTTP 500 error'); }); + + it('returns generic HTTP error when text parsing fails', async () => { + const response = { + status: 500, + headers: { + get: jest.fn().mockReturnValue('text/plain'), + }, + text: jest.fn().mockRejectedValue(new Error('Read error')), + } as unknown as Response; + + const error = await getErrorFromResponse(response); + + expect(error.message).toBe('HTTP 500 error'); + }); }); describe('createSentryError', () => { diff --git a/packages/claims-controller/src/utils.ts b/packages/claims-controller/src/utils.ts index da6bc42c6d9..0f75df69cfe 100644 --- a/packages/claims-controller/src/utils.ts +++ b/packages/claims-controller/src/utils.ts @@ -5,11 +5,25 @@ * @returns An error. */ export async function getErrorFromResponse(response: Response): Promise { + const contentType = response.headers?.get('content-type'); const statusCode = response.status; try { - const json = await response.json(); - const errorMessage = json?.error ?? json?.message ?? 'Unknown error'; - const networkError = `error: ${errorMessage}, statusCode: ${statusCode}`; + if (contentType?.includes('application/json')) { + const json = await response.json(); + const errorMessage = json?.error ?? json?.message ?? 'Unknown error'; + const networkError = `error: ${errorMessage}, statusCode: ${statusCode}`; + return new Error(networkError); + } else if (contentType?.includes('text/plain')) { + const text = await response.text(); + const networkError = `error: ${text}, statusCode: ${statusCode}`; + return new Error(networkError); + } + + const error = + 'data' in response && typeof response.data === 'string' + ? response.data + : 'Unknown error'; + const networkError = `error: ${error}, statusCode: ${statusCode}`; return new Error(networkError); } catch { return new Error(`HTTP ${statusCode} error`);