Skip to content
Open
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/crisp-buckets-agree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@human-protocol/sdk": minor
---

Added typed subgraph errors (SubgraphRequestError, SubgraphBadIndexerError) and wrapped subgraph request failures with these classes
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
HttpException,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { SubgraphRequestError } from '@human-protocol/sdk';

import logger from '../../logger';

Expand All @@ -23,7 +24,15 @@ export class ExceptionFilter implements IExceptionFilter {
message: 'Internal server error',
};

if (exception instanceof HttpException) {
if (exception instanceof SubgraphRequestError) {
status = HttpStatus.BAD_GATEWAY;
responseBody.message = exception.message;

this.logger.error('Unhandled subgraph error', {
error: exception,
path: request.url,
});
} else if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
if (typeof exceptionResponse === 'string') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { SubgraphRequestError } from '@human-protocol/sdk';

import logger from '../../logger';
import {
Expand Down Expand Up @@ -36,6 +37,8 @@ export class ExceptionFilter implements IExceptionFilter {
return HttpStatus.UNPROCESSABLE_ENTITY;
} else if (exception instanceof DatabaseError) {
return HttpStatus.UNPROCESSABLE_ENTITY;
} else if (exception instanceof SubgraphRequestError) {
return HttpStatus.BAD_GATEWAY;
}

const exceptionStatusCode = exception.statusCode || exception.status;
Expand All @@ -51,7 +54,12 @@ export class ExceptionFilter implements IExceptionFilter {
const status = this.getStatus(exception);
const message = exception.message || 'Internal server error';

if (status === HttpStatus.INTERNAL_SERVER_ERROR) {
if (exception instanceof SubgraphRequestError) {
this.logger.error('Subgraph request failed', {
error: exception,
path: request.url,
});
} else if (status === HttpStatus.INTERNAL_SERVER_ERROR) {
this.logger.error('Unhandled exception', {
error: exception,
path: request.url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { SubgraphRequestError } from '@human-protocol/sdk';

import logger from '../../logger';
import {
Expand Down Expand Up @@ -33,6 +34,8 @@ export class ExceptionFilter implements IExceptionFilter {
return HttpStatus.CONFLICT;
} else if (exception instanceof ServerError) {
return HttpStatus.UNPROCESSABLE_ENTITY;
} else if (exception instanceof SubgraphRequestError) {
return HttpStatus.BAD_GATEWAY;
}

const exceptionStatusCode = exception.statusCode || exception.status;
Expand All @@ -48,7 +51,12 @@ export class ExceptionFilter implements IExceptionFilter {
const status = this.getStatus(exception);
const message = exception.message || 'Internal server error';

if (status === HttpStatus.INTERNAL_SERVER_ERROR) {
if (exception instanceof SubgraphRequestError) {
this.logger.error('Subgraph request failed', {
error: exception,
path: request.url,
});
} else if (status === HttpStatus.INTERNAL_SERVER_ERROR) {
this.logger.error('Unhandled exception', {
error: exception,
path: request.url,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
HttpException,
HttpStatus,
} from '@nestjs/common';
import { SubgraphRequestError } from '@human-protocol/sdk';
import logger from '../../logger';
import { AxiosError } from 'axios';
import * as errorUtils from '../utils/error';
Expand All @@ -21,7 +22,15 @@ export class ExceptionFilter implements IExceptionFilter {
let status = HttpStatus.INTERNAL_SERVER_ERROR;
let message: any = 'Internal Server Error';

if (exception instanceof HttpException) {
if (exception instanceof SubgraphRequestError) {
status = HttpStatus.BAD_GATEWAY;
message = exception.message;

this.logger.error('Subgraph request failed', {
error: errorUtils.formatError(exception),
path: request.url,
});
} else if (exception instanceof HttpException) {
status = exception.getStatus();
message = exception.getResponse();
} else if (exception instanceof AxiosError) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { SubgraphRequestError } from '@human-protocol/sdk';

import {
ValidationError,
Expand Down Expand Up @@ -36,6 +37,8 @@ export class ExceptionFilter implements IExceptionFilter {
return HttpStatus.UNPROCESSABLE_ENTITY;
} else if (exception instanceof DatabaseError) {
return HttpStatus.UNPROCESSABLE_ENTITY;
} else if (exception instanceof SubgraphRequestError) {
return HttpStatus.BAD_GATEWAY;
}

const exceptionStatusCode = exception.statusCode || exception.status;
Expand All @@ -51,7 +54,12 @@ export class ExceptionFilter implements IExceptionFilter {
const status = this.getStatus(exception);
const message = exception.message || 'Internal server error';

if (status === HttpStatus.INTERNAL_SERVER_ERROR) {
if (exception instanceof SubgraphRequestError) {
this.logger.error('Subgraph request failed', {
error: exception,
path: request.url,
});
} else if (status === HttpStatus.INTERNAL_SERVER_ERROR) {
this.logger.error('Unhandled exception', {
error: exception,
path: request.url,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { SubgraphRequestError } from '@human-protocol/sdk';
import {
ArgumentsHost,
Catch,
Expand Down Expand Up @@ -31,6 +32,13 @@ export class ExceptionFilter implements IExceptionFilter {
responseBody.message = 'Unprocessable entity';
}
this.logger.error('Database error', exception);
} else if (exception instanceof SubgraphRequestError) {
status = HttpStatus.BAD_GATEWAY;
responseBody.message = exception.message;
this.logger.error('Subgraph request failed', {
error: exception,
path: request.url,
});
} else if (exception instanceof HttpException) {
status = exception.getStatus();
const exceptionResponse = exception.getResponse();
Expand Down
14 changes: 14 additions & 0 deletions packages/sdk/typescript/human-protocol-sdk/src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,17 @@ export class InvalidKeyError extends Error {
super(`Key "${key}" not found for address ${address}`);
}
}

export class SubgraphRequestError extends Error {
public readonly statusCode?: number;
public readonly url: string;

constructor(message: string, url: string, statusCode?: number) {
super(message);
this.name = this.constructor.name;
this.url = url;
this.statusCode = statusCode;
}
}

export class SubgraphBadIndexerError extends SubgraphRequestError {}
2 changes: 2 additions & 0 deletions packages/sdk/typescript/human-protocol-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export {
ContractExecutionError,
InvalidEthereumAddressError,
InvalidKeyError,
SubgraphBadIndexerError,
SubgraphRequestError,
} from './error';

export {
Expand Down
45 changes: 42 additions & 3 deletions packages/sdk/typescript/human-protocol-sdk/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
NonceExpired,
NumericFault,
ReplacementUnderpriced,
SubgraphBadIndexerError,
SubgraphRequestError,
TransactionReplaced,
WarnSubgraphApiKeyNotProvided,
} from './error';
Expand Down Expand Up @@ -117,6 +119,38 @@ export const isIndexerError = (error: any): boolean => {
return errorMessage.toLowerCase().includes('bad indexers');
};

const getSubgraphErrorMessage = (error: any): string => {
return (
error?.response?.errors?.[0]?.message ||
error?.message ||
error?.toString?.() ||
'Subgraph request failed'
);
};

const getSubgraphStatusCode = (error: any): number | undefined => {
if (typeof error?.response?.status === 'number') {
return error.response.status;
}

if (typeof error?.status === 'number') {
return error.status;
}

return undefined;
};

const toSubgraphError = (error: any, url: string): Error => {
const message = getSubgraphErrorMessage(error);
const statusCode = getSubgraphStatusCode(error);

if (isIndexerError(error)) {
return new SubgraphBadIndexerError(message, url, statusCode);
}

return new SubgraphRequestError(message, url, statusCode);
};

const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
Expand Down Expand Up @@ -154,7 +188,11 @@ export const customGqlFetch = async <T = any>(
: undefined;

if (!options) {
return await gqlFetch<T>(url, query, variables, headers);
try {
return await gqlFetch<T>(url, query, variables, headers);
} catch (error) {
throw toSubgraphError(error, url);
}
}

const hasMaxRetries = options.maxRetries !== undefined;
Expand All @@ -177,10 +215,11 @@ export const customGqlFetch = async <T = any>(
try {
return await gqlFetch<T>(targetUrl, query, variables, headers);
} catch (error) {
lastError = error;
const wrappedError = toSubgraphError(error, targetUrl);
lastError = wrappedError;

if (attempt === maxRetries || !isIndexerError(error)) {
throw error;
throw wrappedError;
}

const delay = baseDelay * attempt;
Expand Down
20 changes: 17 additions & 3 deletions packages/sdk/typescript/human-protocol-sdk/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
WarnSubgraphApiKeyNotProvided,
ErrorRetryParametersMissing,
ErrorRoutingRequestsToIndexerRequiresApiKey,
SubgraphBadIndexerError,
SubgraphRequestError,
} from '../src/error';
import {
getSubgraphUrl,
Expand Down Expand Up @@ -340,7 +342,7 @@ describe('customGqlFetch', () => {
maxRetries: 3,
baseDelay: 10,
})
).rejects.toThrow('Regular GraphQL error');
).rejects.toThrow(SubgraphRequestError);

expect(gqlFetchSpy).toHaveBeenCalledTimes(1);
});
Expand All @@ -360,7 +362,7 @@ describe('customGqlFetch', () => {
maxRetries: 2,
baseDelay: 10,
})
).rejects.toEqual(badIndexerError);
).rejects.toThrow(SubgraphBadIndexerError);

expect(gqlFetchSpy).toHaveBeenCalledTimes(3);
});
Expand Down Expand Up @@ -400,8 +402,20 @@ describe('customGqlFetch', () => {
maxRetries: 1,
baseDelay: 10,
})
).rejects.toEqual(badIndexerError);
).rejects.toThrow(SubgraphBadIndexerError);

expect(gqlFetchSpy).toHaveBeenCalledTimes(2);
});

test('wraps subgraph request errors even when no retry config is provided', async () => {
const gqlFetchSpy = vi
.spyOn(gqlFetch, 'default')
.mockRejectedValue(new Error('fetch failed'));

await expect(
customGqlFetch(mockUrl, mockQuery, mockVariables)
).rejects.toThrow(SubgraphRequestError);

expect(gqlFetchSpy).toHaveBeenCalledTimes(1);
});
});
Loading