Skip to content

Commit f75b831

Browse files
authored
Refactor error handling across commands (#2)
* Refactor error handling across commands * Format * Improve rate limit error
1 parent bf11676 commit f75b831

File tree

8 files changed

+463
-22
lines changed

8 files changed

+463
-22
lines changed

src/commands/apikeys.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Command } from 'commander'
22
import chalk from 'chalk'
33
import { listAccessKeys, getDefaultAccessKey } from '../lib/api.js'
44
import { isLoggedIn, EXIT_CODES } from '../lib/config.js'
5+
import { extractErrorMessage } from '../lib/errors.js'
56

67
export const apikeysCommand = new Command('apikeys')
78
.description('Manage API keys for a project')
@@ -103,7 +104,7 @@ async function listApiKeysAction(
103104

104105
console.log('')
105106
} catch (error) {
106-
const errorMessage = error instanceof Error ? error.message : String(error)
107+
const errorMessage = extractErrorMessage(error)
107108
if (json) {
108109
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
109110
} else {
@@ -172,7 +173,7 @@ async function getDefaultKeyAction(
172173
)
173174
console.log('')
174175
} catch (error) {
175-
const errorMessage = error instanceof Error ? error.message : String(error)
176+
const errorMessage = extractErrorMessage(error)
176177
if (json) {
177178
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
178179
} else {

src/commands/indexer.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { SequenceIndexer } from '@0xsequence/indexer'
44
import { networks, ChainId } from '@0xsequence/network'
55
import { EXIT_CODES } from '../lib/config.js'
66
import { ethers } from 'ethers'
7+
import { extractErrorMessage } from '../lib/errors.js'
78

89
// Get indexer URL for a chain
910
function getIndexerUrl(chainId: number): string {
@@ -77,7 +78,7 @@ indexerCommand
7778
}
7879
console.log('')
7980
} catch (error) {
80-
const errorMessage = error instanceof Error ? error.message : String(error)
81+
const errorMessage = extractErrorMessage(error)
8182
if (json) {
8283
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.GENERAL_ERROR }))
8384
} else {
@@ -147,7 +148,7 @@ indexerCommand
147148
console.log(chalk.cyan(` ${symbol}:`), chalk.white(balance))
148149
console.log('')
149150
} catch (error) {
150-
const errorMessage = error instanceof Error ? error.message : String(error)
151+
const errorMessage = extractErrorMessage(error)
151152
if (json) {
152153
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.GENERAL_ERROR }))
153154
} else {
@@ -237,7 +238,7 @@ indexerCommand
237238
console.log('')
238239
}
239240
} catch (error) {
240-
const errorMessage = error instanceof Error ? error.message : String(error)
241+
const errorMessage = extractErrorMessage(error)
241242
if (json) {
242243
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.GENERAL_ERROR }))
243244
} else {
@@ -327,7 +328,7 @@ indexerCommand
327328
}
328329
console.log('')
329330
} catch (error) {
330-
const errorMessage = error instanceof Error ? error.message : String(error)
331+
const errorMessage = extractErrorMessage(error)
331332
if (json) {
332333
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.GENERAL_ERROR }))
333334
} else {

src/commands/login.ts

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Command } from 'commander'
22
import chalk from 'chalk'
33
import { generateEthAuthProof } from '../lib/ethauth.js'
4-
import { getAuthToken } from '../lib/api.js'
4+
import { getAuthToken, isApiError } from '../lib/api.js'
5+
import { extractErrorMessage } from '../lib/errors.js'
56
import {
67
updateConfig,
78
EXIT_CODES,
@@ -116,7 +117,71 @@ export const loginCommand = new Command('login')
116117
console.log(chalk.gray(' sequence-builder projects create "My Project"'))
117118
console.log('')
118119
} catch (error) {
119-
const errorMessage = error instanceof Error ? error.message : String(error)
120+
const errorMessage = extractErrorMessage(error)
121+
122+
// Structured API error with rate-limit / permission info
123+
if (isApiError(error) && (error.isRateLimited || error.isPermissionDenied)) {
124+
if (options.json) {
125+
console.log(
126+
JSON.stringify({
127+
error: error.isRateLimited ? 'Rate limited' : 'Permission denied',
128+
statusCode: error.statusCode,
129+
retryAfterSeconds: error.retryAfterSeconds,
130+
detail: error.errorBody,
131+
code: EXIT_CODES.API_ERROR,
132+
})
133+
)
134+
} else {
135+
if (error.isRateLimited) {
136+
console.error(chalk.red('✖ Rate limited by the API'))
137+
if (error.retryAfterSeconds !== null) {
138+
console.error(chalk.yellow(` Retry after: ${error.retryAfterSeconds}s`))
139+
}
140+
console.error(
141+
chalk.gray(
142+
' You have made too many login attempts. Please wait before trying again.'
143+
)
144+
)
145+
} else {
146+
console.error(chalk.red('✖ Permission denied (403)'))
147+
console.error(chalk.gray(' This can happen when:'))
148+
console.error(chalk.gray(' - Too many signing/login attempts in a short period'))
149+
console.error(chalk.gray(' - The ETHAuth proof is malformed or expired'))
150+
console.error(chalk.gray(' - Your wallet address is not authorized'))
151+
}
152+
if (error.retryAfterSeconds !== null && !error.isRateLimited) {
153+
console.error(chalk.yellow(` Retry after: ${error.retryAfterSeconds}s`))
154+
}
155+
if (error.errorBody) {
156+
console.error(chalk.gray(` Server response: ${error.errorBody}`))
157+
}
158+
}
159+
process.exit(EXIT_CODES.API_ERROR)
160+
}
161+
162+
// Catch 403/rate-limit in generic error strings (e.g. from SDK internals)
163+
if (
164+
errorMessage.includes('403') ||
165+
errorMessage.toLowerCase().includes('permissiondenied') ||
166+
errorMessage.toLowerCase().includes('rate limit')
167+
) {
168+
if (options.json) {
169+
console.log(
170+
JSON.stringify({
171+
error: 'Permission denied or rate limited',
172+
detail: errorMessage,
173+
code: EXIT_CODES.API_ERROR,
174+
})
175+
)
176+
} else {
177+
console.error(chalk.red('✖ Permission denied or rate limited'))
178+
console.error(chalk.gray(' This can happen when:'))
179+
console.error(chalk.gray(' - Too many signing/login attempts in a short period'))
180+
console.error(chalk.gray(' - The ETHAuth proof is malformed or expired'))
181+
console.error(chalk.gray(` Detail: ${errorMessage}`))
182+
}
183+
process.exit(EXIT_CODES.API_ERROR)
184+
}
120185

121186
if (options.json) {
122187
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))

src/commands/projects.ts

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,73 @@
11
import { Command } from 'commander'
22
import chalk from 'chalk'
33
import { Session } from '@0xsequence/auth'
4-
import { listProjects, createProject, getProject, getDefaultAccessKey } from '../lib/api.js'
4+
import {
5+
listProjects,
6+
createProject,
7+
getProject,
8+
getDefaultAccessKey,
9+
isApiError,
10+
} from '../lib/api.js'
11+
import { extractErrorMessage } from '../lib/errors.js'
512
import { isLoggedIn, EXIT_CODES, loadConfig } from '../lib/config.js'
613
import { isValidPrivateKey } from '../lib/wallet.js'
714

15+
/**
16+
* Handle API errors with rate-limit / permission awareness.
17+
* Returns true if the error was handled; false otherwise.
18+
*/
19+
function handleApiErrorOutput(error: unknown, json: boolean): boolean {
20+
if (
21+
isApiError(error) &&
22+
(error.isRateLimited || error.isPermissionDenied || error.isUnauthorized)
23+
) {
24+
if (json) {
25+
console.log(
26+
JSON.stringify({
27+
error: error.isRateLimited
28+
? 'Rate limited'
29+
: error.isPermissionDenied
30+
? 'Permission denied'
31+
: 'Unauthorized',
32+
statusCode: error.statusCode,
33+
retryAfterSeconds: error.retryAfterSeconds,
34+
detail: error.errorBody,
35+
code: EXIT_CODES.API_ERROR,
36+
})
37+
)
38+
} else {
39+
if (error.isRateLimited) {
40+
console.error(chalk.red('✖ Rate limited by the API'))
41+
if (error.retryAfterSeconds !== null) {
42+
console.error(chalk.yellow(` Retry after: ${error.retryAfterSeconds}s`))
43+
}
44+
console.error(
45+
chalk.gray(' You have made too many requests. Please wait before trying again.')
46+
)
47+
} else if (error.isPermissionDenied) {
48+
console.error(chalk.red('✖ Permission denied (403)'))
49+
console.error(chalk.gray(' This can happen when:'))
50+
console.error(chalk.gray(' - Your session token has expired (re-run login)'))
51+
console.error(chalk.gray(' - Too many requests in a short period'))
52+
console.error(chalk.gray(' - The access key is invalid or revoked'))
53+
if (error.retryAfterSeconds !== null) {
54+
console.error(chalk.yellow(` Retry after: ${error.retryAfterSeconds}s`))
55+
}
56+
} else {
57+
console.error(chalk.red('✖ Unauthorized (401)'))
58+
console.error(
59+
chalk.gray(' Your JWT token may have expired. Re-run: sequence-builder login')
60+
)
61+
}
62+
if (error.errorBody) {
63+
console.error(chalk.gray(` Server response: ${error.errorBody}`))
64+
}
65+
}
66+
return true
67+
}
68+
return false
69+
}
70+
871
export const projectsCommand = new Command('projects')
972
.description('Manage Sequence Builder projects')
1073
.option('--json', 'Output in JSON format')
@@ -103,7 +166,10 @@ async function listProjectsAction(options: { json?: boolean; env?: string; apiUr
103166
console.log(chalk.gray('Run `sequence-builder apikeys <project-id>` to view API keys'))
104167
console.log('')
105168
} catch (error) {
106-
const errorMessage = error instanceof Error ? error.message : String(error)
169+
if (handleApiErrorOutput(error, !!json)) {
170+
process.exit(EXIT_CODES.API_ERROR)
171+
}
172+
const errorMessage = extractErrorMessage(error)
107173
if (json) {
108174
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
109175
} else {
@@ -233,7 +299,10 @@ async function createProjectAction(
233299
}
234300
console.log('')
235301
} catch (error) {
236-
const errorMessage = error instanceof Error ? error.message : String(error)
302+
if (handleApiErrorOutput(error, !!json)) {
303+
process.exit(EXIT_CODES.API_ERROR)
304+
}
305+
const errorMessage = extractErrorMessage(error)
237306
if (json) {
238307
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
239308
} else {
@@ -296,7 +365,10 @@ async function getProjectAction(
296365
}
297366
console.log('')
298367
} catch (error) {
299-
const errorMessage = error instanceof Error ? error.message : String(error)
368+
if (handleApiErrorOutput(error, !!json)) {
369+
process.exit(EXIT_CODES.API_ERROR)
370+
}
371+
const errorMessage = extractErrorMessage(error)
300372
if (json) {
301373
console.log(JSON.stringify({ error: errorMessage, code: EXIT_CODES.API_ERROR }))
302374
} else {

0 commit comments

Comments
 (0)