diff --git a/apps/api/src/pkg/keys/service.ts b/apps/api/src/pkg/keys/service.ts index 15c03d0223..93a8c7e558 100644 --- a/apps/api/src/pkg/keys/service.ts +++ b/apps/api/src/pkg/keys/service.ts @@ -26,11 +26,11 @@ export class DisabledWorkspaceError extends BaseError<{ workspaceId: string }> { export class MissingRatelimitError extends BaseError<{ name: string }> { public readonly retry = false; public readonly name = MissingRatelimitError.name; - constructor(name: string) { + constructor(ratelimitName: string, message: string) { super({ - message: `ratelimit "${name}" does not exist`, + message, context: { - name, + name: ratelimitName, }, }); } @@ -504,7 +504,14 @@ export class KeyService { continue; } - return Err(new MissingRatelimitError(r.name)); + let errorMessage = `ratelimit "${r.name}" was requested but does not exist for key "${data.key.id}"`; + if (data.identity) { + errorMessage += ` nor identity { id: ${data.identity.id}, externalId: ${data.identity.externalId}}`; + } else { + errorMessage += " and there is no identity connected"; + } + + return Err(new MissingRatelimitError(r.name, errorMessage)); } const [pass, ratelimit] = await this.ratelimit(c, data.key, ratelimits); diff --git a/apps/api/src/routes/v1_keys_verifyKey.error.test.ts b/apps/api/src/routes/v1_keys_verifyKey.error.test.ts new file mode 100644 index 0000000000..fb30e8385a --- /dev/null +++ b/apps/api/src/routes/v1_keys_verifyKey.error.test.ts @@ -0,0 +1,94 @@ +import { describe, expect, test } from "vitest"; + +import type { ErrorResponse } from "@/pkg/errors"; +import { schema } from "@unkey/db"; +import { newId } from "@unkey/id"; +import { IntegrationHarness } from "src/pkg/testutil/integration-harness"; + +describe("with identity", () => { + describe("with ratelimits", () => { + describe("missing ratelimit", () => { + test("returns 400 and a useful error message", async (t) => { + const h = await IntegrationHarness.init(t); + + const identity = { + id: newId("test"), + workspaceId: h.resources.userWorkspace.id, + externalId: newId("test"), + }; + await h.db.primary.insert(schema.identities).values(identity); + await h.db.primary.insert(schema.ratelimits).values({ + id: newId("test"), + workspaceId: h.resources.userWorkspace.id, + name: "existing-ratelimit", + identityId: identity.id, + limit: 100, + duration: 60_000, + }); + + const key = await h.createKey({ identityId: identity.id }); + + const res = await h.post({ + url: "/v1/keys.verifyKey", + headers: { + "Content-Type": "application/json", + }, + body: { + key: key.key, + ratelimits: [ + { + name: "does-not-exist", + }, + ], + }, + }); + + expect(res.status).toEqual(400); + expect(res.body.error.message).toMatchInlineSnapshot( + `"ratelimit "does-not-exist" was requested but does not exist for key "test_2BC2hsZxB7P9WLR37L8K4jSwMugv" nor identity { id: test_2BC2hZebF4du9PYnijrmSryNP9xe, externalId: test_2BC2hZjbrfj1Zb1HgudRYbonQzpx}"`, + ); + }); + }); + }); +}); + +describe("without identity", () => { + describe("with ratelimits", () => { + describe("missing ratelimit", () => { + test("returns 400 and a useful error message", async (t) => { + const h = await IntegrationHarness.init(t); + + const key = await h.createKey(); + + await h.db.primary.insert(schema.ratelimits).values({ + id: newId("test"), + workspaceId: h.resources.userWorkspace.id, + name: "existing-ratelimit", + keyId: key.keyId, + limit: 100, + duration: 60_000, + }); + + const res = await h.post({ + url: "/v1/keys.verifyKey", + headers: { + "Content-Type": "application/json", + }, + body: { + key: key.key, + ratelimits: [ + { + name: "does-not-exist", + }, + ], + }, + }); + + expect(res.status).toEqual(400); + expect(res.body.error.message).toMatchInlineSnapshot( + `"ratelimit "does-not-exist" was requested but does not exist for key "test_2BC2mdMQYnubdoAsBb2PD7MhooPs" and there is no identity connected"`, + ); + }); + }); + }); +});