Skip to content

Commit 597a9df

Browse files
fmenezesCopilot
andauthored
fix: improve api error messages (#176)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 75f4fa3 commit 597a9df

File tree

4 files changed

+215
-49
lines changed

4 files changed

+215
-49
lines changed

scripts/apply.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ async function main() {
9393
.map((operation) => {
9494
const { operationId, method, path, requiredParams, hasResponseBody } = operation;
9595
return `async ${operationId}(options${requiredParams ? "" : "?"}: FetchOptions<operations["${operationId}"]>) {
96-
${hasResponseBody ? `const { data } = ` : ``}await this.client.${method}("${path}", options);
96+
const { ${hasResponseBody ? `data, ` : ``}error, response } = await this.client.${method}("${path}", options);
97+
if (error) {
98+
throw ApiClientError.fromError(response, error);
99+
}
97100
${
98101
hasResponseBody
99102
? `return data;

src/common/atlas/apiClient.ts

Lines changed: 86 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,6 @@ export class ApiClient {
5555
},
5656
};
5757

58-
private readonly errorMiddleware: Middleware = {
59-
async onResponse({ response }) {
60-
if (!response.ok) {
61-
throw await ApiClientError.fromResponse(response);
62-
}
63-
},
64-
};
65-
6658
constructor(options: ApiClientOptions) {
6759
this.options = {
6860
...options,
@@ -91,7 +83,6 @@ export class ApiClient {
9183
});
9284
this.client.use(this.authMiddleware);
9385
}
94-
this.client.use(this.errorMiddleware);
9586
}
9687

9788
public hasCredentials(): boolean {
@@ -151,83 +142,152 @@ export class ApiClient {
151142

152143
// DO NOT EDIT. This is auto-generated code.
153144
async listClustersForAllProjects(options?: FetchOptions<operations["listClustersForAllProjects"]>) {
154-
const { data } = await this.client.GET("/api/atlas/v2/clusters", options);
145+
const { data, error, response } = await this.client.GET("/api/atlas/v2/clusters", options);
146+
if (error) {
147+
throw ApiClientError.fromError(response, error);
148+
}
155149
return data;
156150
}
157151

158152
async listProjects(options?: FetchOptions<operations["listProjects"]>) {
159-
const { data } = await this.client.GET("/api/atlas/v2/groups", options);
153+
const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
154+
if (error) {
155+
throw ApiClientError.fromError(response, error);
156+
}
160157
return data;
161158
}
162159

163160
async createProject(options: FetchOptions<operations["createProject"]>) {
164-
const { data } = await this.client.POST("/api/atlas/v2/groups", options);
161+
const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
162+
if (error) {
163+
throw ApiClientError.fromError(response, error);
164+
}
165165
return data;
166166
}
167167

168168
async deleteProject(options: FetchOptions<operations["deleteProject"]>) {
169-
await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
169+
const { error, response } = await this.client.DELETE("/api/atlas/v2/groups/{groupId}", options);
170+
if (error) {
171+
throw ApiClientError.fromError(response, error);
172+
}
170173
}
171174

172175
async getProject(options: FetchOptions<operations["getProject"]>) {
173-
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
176+
const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
177+
if (error) {
178+
throw ApiClientError.fromError(response, error);
179+
}
174180
return data;
175181
}
176182

177183
async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
178-
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
184+
const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
185+
if (error) {
186+
throw ApiClientError.fromError(response, error);
187+
}
179188
return data;
180189
}
181190

182191
async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
183-
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
192+
const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
193+
if (error) {
194+
throw ApiClientError.fromError(response, error);
195+
}
184196
return data;
185197
}
186198

187199
async deleteProjectIpAccessList(options: FetchOptions<operations["deleteProjectIpAccessList"]>) {
188-
await this.client.DELETE("/api/atlas/v2/groups/{groupId}/accessList/{entryValue}", options);
200+
const { error, response } = await this.client.DELETE(
201+
"/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
202+
options
203+
);
204+
if (error) {
205+
throw ApiClientError.fromError(response, error);
206+
}
189207
}
190208

191209
async listClusters(options: FetchOptions<operations["listClusters"]>) {
192-
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
210+
const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
211+
if (error) {
212+
throw ApiClientError.fromError(response, error);
213+
}
193214
return data;
194215
}
195216

196217
async createCluster(options: FetchOptions<operations["createCluster"]>) {
197-
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
218+
const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
219+
if (error) {
220+
throw ApiClientError.fromError(response, error);
221+
}
198222
return data;
199223
}
200224

201225
async deleteCluster(options: FetchOptions<operations["deleteCluster"]>) {
202-
await this.client.DELETE("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options);
226+
const { error, response } = await this.client.DELETE(
227+
"/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
228+
options
229+
);
230+
if (error) {
231+
throw ApiClientError.fromError(response, error);
232+
}
203233
}
204234

205235
async getCluster(options: FetchOptions<operations["getCluster"]>) {
206-
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options);
236+
const { data, error, response } = await this.client.GET(
237+
"/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
238+
options
239+
);
240+
if (error) {
241+
throw ApiClientError.fromError(response, error);
242+
}
207243
return data;
208244
}
209245

210246
async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
211-
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
247+
const { data, error, response } = await this.client.GET(
248+
"/api/atlas/v2/groups/{groupId}/databaseUsers",
249+
options
250+
);
251+
if (error) {
252+
throw ApiClientError.fromError(response, error);
253+
}
212254
return data;
213255
}
214256

215257
async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
216-
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
258+
const { data, error, response } = await this.client.POST(
259+
"/api/atlas/v2/groups/{groupId}/databaseUsers",
260+
options
261+
);
262+
if (error) {
263+
throw ApiClientError.fromError(response, error);
264+
}
217265
return data;
218266
}
219267

220268
async deleteDatabaseUser(options: FetchOptions<operations["deleteDatabaseUser"]>) {
221-
await this.client.DELETE("/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}", options);
269+
const { error, response } = await this.client.DELETE(
270+
"/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
271+
options
272+
);
273+
if (error) {
274+
throw ApiClientError.fromError(response, error);
275+
}
222276
}
223277

224278
async listOrganizations(options?: FetchOptions<operations["listOrganizations"]>) {
225-
const { data } = await this.client.GET("/api/atlas/v2/orgs", options);
279+
const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
280+
if (error) {
281+
throw ApiClientError.fromError(response, error);
282+
}
226283
return data;
227284
}
228285

229286
async listOrganizationProjects(options: FetchOptions<operations["listOrganizationProjects"]>) {
230-
const { data } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
287+
const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
288+
if (error) {
289+
throw ApiClientError.fromError(response, error);
290+
}
231291
return data;
232292
}
233293

src/common/atlas/apiClientError.ts

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,67 @@
1-
export class ApiClientError extends Error {
2-
response?: Response;
1+
import { ApiError } from "./openapi.js";
32

4-
constructor(message: string, response: Response | undefined = undefined) {
3+
export class ApiClientError extends Error {
4+
private constructor(
5+
message: string,
6+
public readonly apiError?: ApiError
7+
) {
58
super(message);
69
this.name = "ApiClientError";
7-
this.response = response;
810
}
911

1012
static async fromResponse(
1113
response: Response,
1214
message: string = `error calling Atlas API`
1315
): Promise<ApiClientError> {
16+
const err = await this.extractError(response);
17+
18+
return this.fromError(response, err, message);
19+
}
20+
21+
static fromError(
22+
response: Response,
23+
error?: ApiError | string | Error,
24+
message: string = `error calling Atlas API`
25+
): ApiClientError {
26+
const errorMessage = this.buildErrorMessage(error);
27+
28+
const apiError = typeof error === "object" && !(error instanceof Error) ? error : undefined;
29+
30+
return new ApiClientError(`[${response.status} ${response.statusText}] ${message}: ${errorMessage}`, apiError);
31+
}
32+
33+
private static async extractError(response: Response): Promise<ApiError | string | undefined> {
1434
try {
15-
const text = await response.text();
16-
return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response);
35+
return (await response.json()) as ApiError;
1736
} catch {
18-
return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response);
37+
try {
38+
return await response.text();
39+
} catch {
40+
return undefined;
41+
}
1942
}
2043
}
44+
45+
private static buildErrorMessage(error?: string | ApiError | Error): string {
46+
let errorMessage: string = "unknown error";
47+
48+
if (error instanceof Error) {
49+
return error.message;
50+
}
51+
52+
//eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
53+
switch (typeof error) {
54+
case "object":
55+
errorMessage = error.reason || "unknown error";
56+
if (error.detail && error.detail.length > 0) {
57+
errorMessage = `${errorMessage}; ${error.detail}`;
58+
}
59+
break;
60+
case "string":
61+
errorMessage = error;
62+
break;
63+
}
64+
65+
return errorMessage.trim();
66+
}
2167
}

0 commit comments

Comments
 (0)