Skip to content

Commit 6ee04a2

Browse files
committed
Handle code-less JSON RPC errors
1 parent 997cfe0 commit 6ee04a2

File tree

6 files changed

+88
-37
lines changed

6 files changed

+88
-37
lines changed

.changeset/moody-otters-enjoy.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@solana/errors': patch
3+
---
4+
5+
Gracefully handle JSON RPC errors that do not provide a `code` attribute in their response

packages/errors/src/__tests__/json-rpc-error-test.ts

+13
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_LEN_MISMATCH,
2121
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_SIGNATURE_VERIFICATION_FAILURE,
2222
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION,
23+
SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR,
2324
SolanaErrorCode,
2425
} from '../codes';
2526
import { SolanaErrorContext } from '../context';
@@ -40,6 +41,18 @@ describe('getSolanaErrorFromJsonRpcError', () => {
4041
const error = getSolanaErrorFromJsonRpcError({ code, message: 'o no' });
4142
expect(error).toHaveProperty('context.__code', 123);
4243
});
44+
it('produces a `SOLANA_ERROR__UNRECOGNIZED_JSON_RPC_ERROR` when no code is given', () => {
45+
const error = getSolanaErrorFromJsonRpcError({ foo: 'bar', message: 'o no' });
46+
expect(error).toHaveProperty('context.__code', SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR);
47+
expect(error).toHaveProperty('context.error', { foo: 'bar', message: 'o no' });
48+
expect(error).toHaveProperty('context.message', 'o no');
49+
});
50+
it('produces a `SOLANA_ERROR__UNRECOGNIZED_JSON_RPC_ERROR` with a fallback message when none is provided', () => {
51+
const error = getSolanaErrorFromJsonRpcError(null);
52+
expect(error).toHaveProperty('context.__code', SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR);
53+
expect(error).toHaveProperty('context.error', null);
54+
expect(error).toHaveProperty('context.message', 'Malformed JSON-RPC error with no message attribute');
55+
});
4356
describe.each([
4457
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_MIN_CONTEXT_SLOT_NOT_REACHED,
4558
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_NODE_UNHEALTHY,

packages/errors/src/codes.ts

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export const SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE = 6;
3434
export const SOLANA_ERROR__MALFORMED_BIGINT_STRING = 7;
3535
export const SOLANA_ERROR__MALFORMED_NUMBER_STRING = 8;
3636
export const SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE = 9;
37+
export const SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR = 10;
3738

3839
// JSON-RPC-related errors.
3940
// Reserve error codes in the range [-32768, -32000]
@@ -453,6 +454,7 @@ export type SolanaErrorCode =
453454
| typeof SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE
454455
| typeof SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE
455456
| typeof SOLANA_ERROR__MALFORMED_BIGINT_STRING
457+
| typeof SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR
456458
| typeof SOLANA_ERROR__MALFORMED_NUMBER_STRING
457459
| typeof SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND
458460
| typeof SOLANA_ERROR__RPC__API_PLAN_MISSING_FOR_RPC_METHOD

packages/errors/src/context.ts

+5
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ import {
125125
SOLANA_ERROR__KEYS__INVALID_SIGNATURE_BYTE_LENGTH,
126126
SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
127127
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
128+
SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR,
128129
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
129130
SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND,
130131
SOLANA_ERROR__RPC__API_PLAN_MISSING_FOR_RPC_METHOD,
@@ -485,6 +486,10 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
485486
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: {
486487
value: string;
487488
};
489+
[SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR]: {
490+
error: unknown;
491+
message: string;
492+
};
488493
[SOLANA_ERROR__MALFORMED_NUMBER_STRING]: {
489494
value: string;
490495
};

packages/errors/src/json-rpc-error.ts

+61-37
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED,
1515
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE,
1616
SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION,
17+
SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR,
1718
SolanaErrorCode,
1819
} from './codes';
1920
import { SolanaErrorContext } from './context';
@@ -91,46 +92,69 @@ export interface RpcSimulateTransactionResult {
9192
unitsConsumed: number | null;
9293
}
9394

94-
export function getSolanaErrorFromJsonRpcError({ code: rawCode, data, message }: RpcErrorResponse): SolanaError {
95+
export function getSolanaErrorFromJsonRpcError(putativeErrorResponse: unknown): SolanaError {
9596
let out: SolanaError;
96-
const code = Number(rawCode);
97-
if (code === SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE) {
98-
const { err, ...preflightErrorContext } = data as RpcSimulateTransactionResult;
99-
const causeObject = err ? { cause: getSolanaErrorFromTransactionError(err) } : null;
100-
out = new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, {
101-
...preflightErrorContext,
102-
...causeObject,
103-
});
104-
} else {
105-
let errorContext;
106-
switch (code) {
107-
case SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR:
108-
case SOLANA_ERROR__JSON_RPC__INVALID_PARAMS:
109-
case SOLANA_ERROR__JSON_RPC__INVALID_REQUEST:
110-
case SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND:
111-
case SOLANA_ERROR__JSON_RPC__PARSE_ERROR:
112-
case SOLANA_ERROR__JSON_RPC__SCAN_ERROR:
113-
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP:
114-
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE:
115-
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET:
116-
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX:
117-
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED:
118-
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED:
119-
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE:
120-
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION:
121-
// The server supplies no structured data, but rather a pre-formatted message. Put
122-
// the server message in `context` so as not to completely lose the data. The long
123-
// term fix for this is to add data to the server responses and modify the
124-
// messages in `@solana/errors` to be actual format strings.
125-
errorContext = { __serverMessage: message };
126-
break;
127-
default:
128-
if (typeof data === 'object' && !Array.isArray(data)) {
129-
errorContext = data;
130-
}
97+
if (isRpcErrorResponse(putativeErrorResponse)) {
98+
const { code: rawCode, data, message } = putativeErrorResponse;
99+
const code = Number(rawCode);
100+
if (code === SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE) {
101+
const { err, ...preflightErrorContext } = data as RpcSimulateTransactionResult;
102+
const causeObject = err ? { cause: getSolanaErrorFromTransactionError(err) } : null;
103+
out = new SolanaError(SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SEND_TRANSACTION_PREFLIGHT_FAILURE, {
104+
...preflightErrorContext,
105+
...causeObject,
106+
});
107+
} else {
108+
let errorContext;
109+
switch (code) {
110+
case SOLANA_ERROR__JSON_RPC__INTERNAL_ERROR:
111+
case SOLANA_ERROR__JSON_RPC__INVALID_PARAMS:
112+
case SOLANA_ERROR__JSON_RPC__INVALID_REQUEST:
113+
case SOLANA_ERROR__JSON_RPC__METHOD_NOT_FOUND:
114+
case SOLANA_ERROR__JSON_RPC__PARSE_ERROR:
115+
case SOLANA_ERROR__JSON_RPC__SCAN_ERROR:
116+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_CLEANED_UP:
117+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_NOT_AVAILABLE:
118+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_BLOCK_STATUS_NOT_AVAILABLE_YET:
119+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_KEY_EXCLUDED_FROM_SECONDARY_INDEX:
120+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_LONG_TERM_STORAGE_SLOT_SKIPPED:
121+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_SLOT_SKIPPED:
122+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_TRANSACTION_PRECOMPILE_VERIFICATION_FAILURE:
123+
case SOLANA_ERROR__JSON_RPC__SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION:
124+
// The server supplies no structured data, but rather a pre-formatted message. Put
125+
// the server message in `context` so as not to completely lose the data. The long
126+
// term fix for this is to add data to the server responses and modify the
127+
// messages in `@solana/errors` to be actual format strings.
128+
errorContext = { __serverMessage: message };
129+
break;
130+
default:
131+
if (typeof data === 'object' && !Array.isArray(data)) {
132+
errorContext = data;
133+
}
134+
}
135+
out = new SolanaError(code as SolanaErrorCode, errorContext as SolanaErrorContext[SolanaErrorCode]);
131136
}
132-
out = new SolanaError(code as SolanaErrorCode, errorContext as SolanaErrorContext[SolanaErrorCode]);
137+
} else {
138+
const message =
139+
typeof putativeErrorResponse === 'object' &&
140+
putativeErrorResponse !== null &&
141+
'message' in putativeErrorResponse &&
142+
typeof putativeErrorResponse.message === 'string'
143+
? putativeErrorResponse.message
144+
: 'Malformed JSON-RPC error with no message attribute';
145+
out = new SolanaError(SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR, { error: putativeErrorResponse, message });
133146
}
134147
safeCaptureStackTrace(out, getSolanaErrorFromJsonRpcError);
135148
return out;
136149
}
150+
151+
function isRpcErrorResponse(value: unknown): value is RpcErrorResponse {
152+
return (
153+
typeof value === 'object' &&
154+
value !== null &&
155+
'code' in value &&
156+
'message' in value &&
157+
(typeof value.code === 'number' || typeof value.code === 'bigint') &&
158+
typeof value.message === 'string'
159+
);
160+
}

packages/errors/src/messages.ts

+2
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ import {
141141
SOLANA_ERROR__KEYS__SIGNATURE_STRING_LENGTH_OUT_OF_RANGE,
142142
SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE,
143143
SOLANA_ERROR__MALFORMED_BIGINT_STRING,
144+
SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR,
144145
SOLANA_ERROR__MALFORMED_NUMBER_STRING,
145146
SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND,
146147
SOLANA_ERROR__RPC__API_PLAN_MISSING_FOR_RPC_METHOD,
@@ -449,6 +450,7 @@ export const SolanaErrorMessages: Readonly<{
449450
'Expected base58-encoded signature string of length in the range [64, 88]. Actual length: $actualLength.',
450451
[SOLANA_ERROR__LAMPORTS_OUT_OF_RANGE]: 'Lamports value must be in the range [0, 2e64-1]',
451452
[SOLANA_ERROR__MALFORMED_BIGINT_STRING]: '`$value` cannot be parsed as a `BigInt`',
453+
[SOLANA_ERROR__MALFORMED_JSON_RPC_ERROR]: '$message',
452454
[SOLANA_ERROR__MALFORMED_NUMBER_STRING]: '`$value` cannot be parsed as a `Number`',
453455
[SOLANA_ERROR__NONCE_ACCOUNT_NOT_FOUND]: 'No nonce account could be found at address `$nonceAccountAddress`',
454456
[SOLANA_ERROR__RPC_SUBSCRIPTIONS__CANNOT_CREATE_SUBSCRIPTION_PLAN]:

0 commit comments

Comments
 (0)