Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/contract/output.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 10 additions & 3 deletions src/lib/clients/management.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 4 additions & 3 deletions src/lib/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
32 changes: 26 additions & 6 deletions test/lib/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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");
Expand Down
Loading