Skip to content

fix: improve api error messages #176

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion scripts/apply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ async function main() {
.map((operation) => {
const { operationId, method, path, requiredParams, hasResponseBody } = operation;
return `async ${operationId}(options${requiredParams ? "" : "?"}: FetchOptions<operations["${operationId}"]>) {
${hasResponseBody ? `const { data } = ` : ``}await this.client.${method}("${path}", options);
const { ${hasResponseBody ? `data, ` : ``}error, response } = await this.client.${method}("${path}", options);
if (error) {
throw ApiClientError.fromError(response, error);
}
${
hasResponseBody
? `return data;
Expand Down
112 changes: 86 additions & 26 deletions src/common/atlas/apiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,6 @@ export class ApiClient {
},
};

private readonly errorMiddleware: Middleware = {
async onResponse({ response }) {
if (!response.ok) {
throw await ApiClientError.fromResponse(response);
}
},
};

constructor(options: ApiClientOptions) {
this.options = {
...options,
Expand Down Expand Up @@ -91,7 +83,6 @@ export class ApiClient {
});
this.client.use(this.authMiddleware);
}
this.client.use(this.errorMiddleware);
}

public hasCredentials(): boolean {
Expand Down Expand Up @@ -151,83 +142,152 @@ export class ApiClient {

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

async listProjects(options?: FetchOptions<operations["listProjects"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups", options);
const { data, error, response } = await this.client.GET("/api/atlas/v2/groups", options);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

async createProject(options: FetchOptions<operations["createProject"]>) {
const { data } = await this.client.POST("/api/atlas/v2/groups", options);
const { data, error, response } = await this.client.POST("/api/atlas/v2/groups", options);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

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

async getProject(options: FetchOptions<operations["getProject"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}", options);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

async listProjectIpAccessLists(options: FetchOptions<operations["listProjectIpAccessLists"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/accessList", options);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

async createProjectIpAccessList(options: FetchOptions<operations["createProjectIpAccessList"]>) {
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/accessList", options);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

async deleteProjectIpAccessList(options: FetchOptions<operations["deleteProjectIpAccessList"]>) {
await this.client.DELETE("/api/atlas/v2/groups/{groupId}/accessList/{entryValue}", options);
const { error, response } = await this.client.DELETE(
"/api/atlas/v2/groups/{groupId}/accessList/{entryValue}",
options
);
if (error) {
throw ApiClientError.fromError(response, error);
}
}

async listClusters(options: FetchOptions<operations["listClusters"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
const { data, error, response } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters", options);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

async createCluster(options: FetchOptions<operations["createCluster"]>) {
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
const { data, error, response } = await this.client.POST("/api/atlas/v2/groups/{groupId}/clusters", options);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

async deleteCluster(options: FetchOptions<operations["deleteCluster"]>) {
await this.client.DELETE("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options);
const { error, response } = await this.client.DELETE(
"/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
options
);
if (error) {
throw ApiClientError.fromError(response, error);
}
}

async getCluster(options: FetchOptions<operations["getCluster"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/clusters/{clusterName}", options);
const { data, error, response } = await this.client.GET(
"/api/atlas/v2/groups/{groupId}/clusters/{clusterName}",
options
);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

async listDatabaseUsers(options: FetchOptions<operations["listDatabaseUsers"]>) {
const { data } = await this.client.GET("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
const { data, error, response } = await this.client.GET(
"/api/atlas/v2/groups/{groupId}/databaseUsers",
options
);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

async createDatabaseUser(options: FetchOptions<operations["createDatabaseUser"]>) {
const { data } = await this.client.POST("/api/atlas/v2/groups/{groupId}/databaseUsers", options);
const { data, error, response } = await this.client.POST(
"/api/atlas/v2/groups/{groupId}/databaseUsers",
options
);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

async deleteDatabaseUser(options: FetchOptions<operations["deleteDatabaseUser"]>) {
await this.client.DELETE("/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}", options);
const { error, response } = await this.client.DELETE(
"/api/atlas/v2/groups/{groupId}/databaseUsers/{databaseName}/{username}",
options
);
if (error) {
throw ApiClientError.fromError(response, error);
}
}

async listOrganizations(options?: FetchOptions<operations["listOrganizations"]>) {
const { data } = await this.client.GET("/api/atlas/v2/orgs", options);
const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs", options);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

async listOrganizationProjects(options: FetchOptions<operations["listOrganizationProjects"]>) {
const { data } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
const { data, error, response } = await this.client.GET("/api/atlas/v2/orgs/{orgId}/groups", options);
if (error) {
throw ApiClientError.fromError(response, error);
}
return data;
}

Expand Down
60 changes: 53 additions & 7 deletions src/common/atlas/apiClientError.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,67 @@
export class ApiClientError extends Error {
response?: Response;
import { ApiError } from "./openapi.js";

constructor(message: string, response: Response | undefined = undefined) {
export class ApiClientError extends Error {
private constructor(
message: string,
public readonly apiError?: ApiError
) {
super(message);
this.name = "ApiClientError";
this.response = response;
}

static async fromResponse(
response: Response,
message: string = `error calling Atlas API`
): Promise<ApiClientError> {
const err = await this.extractError(response);

return this.fromError(response, err, message);
}

static fromError(
response: Response,
error?: ApiError | string | Error,
message: string = `error calling Atlas API`
): ApiClientError {
const errorMessage = this.buildErrorMessage(error);

const apiError = typeof error === "object" && !(error instanceof Error) ? error : undefined;

return new ApiClientError(`[${response.status} ${response.statusText}] ${message}: ${errorMessage}`, apiError);
}

private static async extractError(response: Response): Promise<ApiError | string | undefined> {
try {
const text = await response.text();
return new ApiClientError(`${message}: [${response.status} ${response.statusText}] ${text}`, response);
return (await response.json()) as ApiError;
} catch {
return new ApiClientError(`${message}: ${response.status} ${response.statusText}`, response);
try {
return await response.text();
} catch {
return undefined;
}
}
}

private static buildErrorMessage(error?: string | ApiError | Error): string {
let errorMessage: string = "unknown error";

if (error instanceof Error) {
return error.message;
}

//eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
switch (typeof error) {
case "object":
errorMessage = error.reason || "unknown error";
if (error.detail && error.detail.length > 0) {
errorMessage = `${errorMessage}; ${error.detail}`;
}
break;
case "string":
errorMessage = error;
break;
}

return errorMessage.trim();
}
}
Loading
Loading