diff --git a/docs/contract/output.md b/docs/contract/output.md index 98c5cff..3ff3e57 100644 --- a/docs/contract/output.md +++ b/docs/contract/output.md @@ -50,7 +50,7 @@ Errors go to stderr, as one JSON document: compact when stderr is not a TTY, ind - API errors keep the API fields as they are: `code`, `type`, `message`, `link`. The CLI never rewrites an API message. - `hint` is optional and CLI owned. It names the next command to run. It is a full sentence, without markdown other than backticks around commands. On an unknown command, the hint names the closest existing command, "Did you mean `meili delete indexes`?", computed from the manifest: the CLI suggests, it never asks. - Errors that come from the CLI itself use codes prefixed with `cli_` and `type` set to `cli`. Current list: `cli_usage`, `cli_config`, `cli_auth_missing`, `cli_timeout`, `cli_network`, `cli_task_failed`, `cli_input_invalid`, `cli_internal`. `cli_internal` is an unexpected error inside the CLI itself, a bug to report, and exits 1. Adding a code is a contract change. -- The Management API does not define an error body in its spec, and the running API answers with `{"error": "Forbidden"}`: a message, no code. For its errors the CLI builds the object itself: `type` is `management`, `code` comes from the HTTP status (`management_unauthorized` 401, `management_forbidden` 403, `management_not_found` 404, `management_unprocessable` 422, `management_error` otherwise), `message` is the API's `error` field when the body has one and the spec's response description otherwise, `link` is absent. When the spec gains an error schema with codes, this rule is replaced by pass-through. +- The Management API does not define an error body in its spec, and the running API answers with `{"error": "Forbidden"}`: a message, no code. For its errors the CLI builds the object itself: `type` is `management`, `code` comes from the HTTP status (`management_unauthorized` 401, `management_forbidden` 403, `management_not_found` 404, `management_unprocessable` 422, `management_error` otherwise), `message` is the API's `error` field when the body has one, the spec's response description otherwise, and the status with the request (`HTTP 404 to POST https://…`) when the spec describes neither, `link` is absent. When the spec gains an error schema with codes, this rule is replaced by pass-through. - Stack traces appear only with `--debug`. ## Warnings diff --git a/src/lib/clients/management.ts b/src/lib/clients/management.ts index fdf98d6..464ad97 100644 --- a/src/lib/clients/management.ts +++ b/src/lib/clients/management.ts @@ -28,13 +28,20 @@ export function createManagementClient(options: ManagementClientOptions): Manage headers: { "User-Agent": options.userAgent }, throwOnError: true, }); - client.interceptors.error.use((error, response, _request, request) => + client.interceptors.error.use((error, response, request, options) => response === undefined - ? new MeiliCliError("cli_network", cannotConnectMessage(options.url, error), { cause: error }) + ? new MeiliCliError( + "cli_network", + cannotConnectMessage(client.getConfig().baseUrl ?? "", error), + { + cause: error, + }, + ) : new ManagementApiError( response.status, error, - RESPONSES[`${request.method} ${request.url}`]?.[String(response.status)], + RESPONSES[`${options.method} ${options.url}`]?.[String(response.status)], + `${options.method} ${request?.url ?? options.url}`, ), ); return client; diff --git a/src/lib/errors.ts b/src/lib/errors.ts index d77c87a..43f7cc4 100644 --- a/src/lib/errors.ts +++ b/src/lib/errors.ts @@ -97,14 +97,15 @@ export interface MeiliCliErrorOptions { /** * A non-2xx answer of the Management API, whose error body has no code * (docs/contract/output.md, "Errors"): the message is the body's `error` - * field, else the document's description of that status, else the status. + * field, else the document's description of that status, else the status + * and the request, `POST https://…`, so that a wrong URL shows. */ export class ManagementApiError extends Error { readonly status: number; - constructor(status: number, body: unknown, description: string | undefined) { + constructor(status: number, body: unknown, description: string | undefined, request: string) { const sent = isRecord(body) && typeof body.error === "string" ? body.error : undefined; - super(sent ?? description ?? `The Management API answered HTTP ${status}.`); + super(sent ?? description ?? `The Management API answered HTTP ${status} to ${request}.`); this.name = "ManagementApiError"; this.status = status; } diff --git a/test/lib/errors.test.ts b/test/lib/errors.test.ts index b6983d2..7c797e6 100644 --- a/test/lib/errors.test.ts +++ b/test/lib/errors.test.ts @@ -20,7 +20,12 @@ import { describe("ManagementApiError", () => { it("takes the API's error field, else the document's description, else the status", () => { const rendered = toRenderedError( - new ManagementApiError(403, { error: "Forbidden" }, "team not accessible"), + new ManagementApiError( + 403, + { error: "Forbidden" }, + "team not accessible", + "POST https://m/api/v2/projects", + ), ); expect(rendered.body).toEqual({ code: "management_forbidden", @@ -29,19 +34,34 @@ describe("ManagementApiError", () => { }); expect(rendered.exitCode).toBe(3); expect( - toRenderedError(new ManagementApiError(422, { errors: {} }, "invalid project attributes")) - .body.message, + toRenderedError( + new ManagementApiError( + 422, + { errors: {} }, + "invalid project attributes", + "POST https://m/api/v2/projects", + ), + ).body.message, ).toBe("invalid project attributes"); - expect(toRenderedError(new ManagementApiError(500, undefined, undefined)).body).toEqual({ + expect( + toRenderedError( + new ManagementApiError(500, undefined, undefined, "POST https://m/api/v2/projects"), + ).body, + ).toEqual({ code: "management_error", type: "management", - message: "The Management API answered HTTP 500.", + message: "The Management API answered HTTP 500 to POST https://m/api/v2/projects.", }); }); it("hints at the token on 401", () => { const rendered = toRenderedError( - new ManagementApiError(401, { error: "Unauthorized" }, "unauthorized"), + new ManagementApiError( + 401, + { error: "Unauthorized" }, + "unauthorized", + "GET https://m/api/v2/me", + ), ); expect(rendered.body.code).toBe("management_unauthorized"); expect(rendered.body.hint).toContain("--management-token");