Skip to content

Commit d292046

Browse files
Improve R2 error messages to be clearer and more actionable (#14479)
1 parent f10d4ad commit d292046

7 files changed

Lines changed: 129 additions & 95 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"wrangler": patch
3+
---
4+
5+
Improve R2 error messages to be clearer and more actionable
6+
7+
Error messages for `r2 bucket lifecycle`, `r2 bucket lock`, `r2 bucket catalog`, and `r2 sql` commands now include the specific flag or argument that is missing or invalid, along with usage examples showing the correct syntax.

‎packages/wrangler/src/__tests__/r2/bucket.test.ts‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,7 +1249,8 @@ describe("r2", () => {
12491249
"r2 bucket catalog compaction enable testBucket testNamespace"
12501250
)
12511251
).rejects.toThrowErrorMatchingInlineSnapshot(
1252-
`[Error: Table name is required when namespace is specified]`
1252+
`[Error: Both namespace and table must be provided together. You specified namespace without table. Retry by running:
1253+
wrangler r2 bucket catalog compaction enable testBucket <namespace> <table>]`
12531254
);
12541255
});
12551256

@@ -1260,7 +1261,8 @@ describe("r2", () => {
12601261
'r2 bucket catalog compaction enable testBucket "" testTable'
12611262
)
12621263
).rejects.toThrowErrorMatchingInlineSnapshot(
1263-
`[Error: Namespace is required when table is specified]`
1264+
`[Error: Both namespace and table must be provided together. You specified table without namespace. Retry by running:
1265+
wrangler r2 bucket catalog compaction enable testBucket <namespace> <table>]`
12641266
);
12651267
});
12661268
});
@@ -3520,7 +3522,7 @@ describe("r2", () => {
35203522
`r2 bucket lock add ${bucketName} --name rule-age --prefix prefix-age --retention-days one`
35213523
)
35223524
).rejects.toThrowErrorMatchingInlineSnapshot(
3523-
`[Error: Days must be a number.]`
3525+
`[Error: The value for --retention-days must be a number. Retry with a numeric value (e.g. --retention-days 30).]`
35243526
);
35253527
});
35263528
it("it should fail an age lock rule using command-line arguments with invalid negative age", async () => {
@@ -3544,7 +3546,7 @@ describe("r2", () => {
35443546
`r2 bucket lock add ${bucketName} --name rule-age --prefix prefix-age --retention-days -10`
35453547
)
35463548
).rejects.toThrowErrorMatchingInlineSnapshot(
3547-
`[Error: Days must be a positive number: -10]`
3549+
`[Error: The value for --retention-days must be a positive number, but received '-10'.]`
35483550
);
35493551
});
35503552
it("it should add a date lock rule using command-line arguments", async () => {
@@ -3585,7 +3587,7 @@ describe("r2", () => {
35853587
`r2 bucket lock add ${bucketName} --name "rule-date" --prefix "prefix-date" --retention-date "January 30, 2025"`
35863588
)
35873589
).rejects.toThrowErrorMatchingInlineSnapshot(
3588-
`[Error: Date must be a valid date in the YYYY-MM-DD format: January 30, 2025]`
3590+
`[Error: The value for --retention-date must be in YYYY-MM-DD format (e.g. 2025-12-31), but received 'January 30, 2025'.]`
35893591
);
35903592
});
35913593
it("it should add an indefinite lock rule using command-line arguments", async () => {
@@ -3669,7 +3671,7 @@ describe("r2", () => {
36693671
`r2 bucket lock add ${bucketName} --name rule-indefinite --prefix prefix-indefinite --retention-indefinite false`
36703672
)
36713673
).rejects.toThrowErrorMatchingInlineSnapshot(
3672-
`[Error: Retention must be specified.]`
3674+
`[Error: No retention specified. Use one of --retention-days <number>, --retention-date <YYYY-MM-DD>, or --retention-indefinite to set a retention.]`
36733675
);
36743676
});
36753677
it("it should fail a lock rule without any command-line arguments", async () => {
@@ -3681,7 +3683,8 @@ describe("r2", () => {
36813683
await expect(() =>
36823684
runWrangler(`r2 bucket lock add ${bucketName}`)
36833685
).rejects.toThrowErrorMatchingInlineSnapshot(
3684-
`[Error: Must specify a rule name.]`
3686+
`[Error: Missing required rule name. Provide a name with --name <rule-name> or as a positional argument:
3687+
wrangler r2 bucket lock add <bucket> <name>]`
36853688
);
36863689
});
36873690
});

‎packages/wrangler/src/__tests__/r2/sql.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe("r2 sql", () => {
7070
it("should validate warehouse name format", async ({ expect }) => {
7171
await expect(
7272
runWrangler(`r2 sql query invalidwarehouse "${mockQuery}"`)
73-
).rejects.toThrow("Warehouse name has invalid format");
73+
).rejects.toThrow("Invalid warehouse name format");
7474
});
7575

7676
it("should execute a successful query and display results", async ({

‎packages/wrangler/src/r2/catalog.ts‎

Lines changed: 58 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,40 @@ import {
2323
upsertR2CatalogCredential,
2424
} from "./helpers/catalog";
2525

26+
/**
27+
* Validates that namespace and table positional args are either both provided or both omitted.
28+
* Throws a {@link UserError} if only one of the two is given.
29+
*
30+
* @param namespace The namespace argument value
31+
* @param table The table argument value
32+
* @param bucket The bucket name (used in the retry hint)
33+
* @param subcommand The subcommand path after "wrangler r2 bucket catalog" (e.g. "compaction enable")
34+
*/
35+
function validateNamespaceAndTable(
36+
namespace: string | undefined,
37+
table: string | undefined,
38+
bucket: string,
39+
subcommand: string
40+
): void {
41+
const telemetryBase = `r2 catalog ${subcommand.replace("-", " ")}`;
42+
if (namespace && !table) {
43+
throw new UserError(
44+
`Both namespace and table must be provided together. You specified namespace without table. Retry by running:\n wrangler r2 bucket catalog ${subcommand} ${bucket} <namespace> <table>`,
45+
{
46+
telemetryMessage: `${telemetryBase} missing table`,
47+
}
48+
);
49+
}
50+
if (!namespace && table) {
51+
throw new UserError(
52+
`Both namespace and table must be provided together. You specified table without namespace. Retry by running:\n wrangler r2 bucket catalog ${subcommand} ${bucket} <namespace> <table>`,
53+
{
54+
telemetryMessage: `${telemetryBase} missing namespace`,
55+
}
56+
);
57+
}
58+
}
59+
2660
export const r2BucketCatalogNamespace = createNamespace({
2761
metadata: {
2862
description:
@@ -219,20 +253,12 @@ export const r2BucketCatalogCompactionEnableCommand = createCommand({
219253
async handler(args, { config }) {
220254
const accountId = await requireAuth(config);
221255

222-
// Validate namespace and table are provided together
223-
if (args.namespace && !args.table) {
224-
throw new UserError(
225-
"Table name is required when namespace is specified",
226-
{
227-
telemetryMessage: "r2 catalog compaction enable missing table",
228-
}
229-
);
230-
}
231-
if (!args.namespace && args.table) {
232-
throw new UserError("Namespace is required when table is specified", {
233-
telemetryMessage: "r2 catalog compaction enable missing namespace",
234-
});
235-
}
256+
validateNamespaceAndTable(
257+
args.namespace,
258+
args.table,
259+
args.bucket,
260+
"compaction enable"
261+
);
236262

237263
if (args.namespace && args.table) {
238264
// Table-level compaction
@@ -312,20 +338,12 @@ export const r2BucketCatalogCompactionDisableCommand = createCommand({
312338
async handler(args, { config }) {
313339
const accountId = await requireAuth(config);
314340

315-
// Validate namespace and table are provided together
316-
if (args.namespace && !args.table) {
317-
throw new UserError(
318-
"Table name is required when namespace is specified",
319-
{
320-
telemetryMessage: "r2 catalog compaction disable missing table",
321-
}
322-
);
323-
}
324-
if (!args.namespace && args.table) {
325-
throw new UserError("Namespace is required when table is specified", {
326-
telemetryMessage: "r2 catalog compaction disable missing namespace",
327-
});
328-
}
341+
validateNamespaceAndTable(
342+
args.namespace,
343+
args.table,
344+
args.bucket,
345+
"compaction disable"
346+
);
329347

330348
if (args.namespace && args.table) {
331349
// Table-level compaction
@@ -422,22 +440,12 @@ export const r2BucketCatalogSnapshotExpirationEnableCommand = createCommand({
422440
async handler(args, { config }) {
423441
const accountId = await requireAuth(config);
424442

425-
// Validate namespace and table are provided together
426-
if (args.namespace && !args.table) {
427-
throw new UserError(
428-
"Table name is required when namespace is specified",
429-
{
430-
telemetryMessage:
431-
"r2 catalog snapshot expiration enable missing table",
432-
}
433-
);
434-
}
435-
if (!args.namespace && args.table) {
436-
throw new UserError("Namespace is required when table is specified", {
437-
telemetryMessage:
438-
"r2 catalog snapshot expiration enable missing namespace",
439-
});
440-
}
443+
validateNamespaceAndTable(
444+
args.namespace,
445+
args.table,
446+
args.bucket,
447+
"snapshot-expiration enable"
448+
);
441449

442450
if (args.namespace && args.table) {
443451
// Table-level snapshot expiration
@@ -526,22 +534,12 @@ export const r2BucketCatalogSnapshotExpirationDisableCommand = createCommand({
526534
async handler(args, { config }) {
527535
const accountId = await requireAuth(config);
528536

529-
// Validate namespace and table are provided together
530-
if (args.namespace && !args.table) {
531-
throw new UserError(
532-
"Table name is required when namespace is specified",
533-
{
534-
telemetryMessage:
535-
"r2 catalog snapshot expiration disable missing table",
536-
}
537-
);
538-
}
539-
if (!args.namespace && args.table) {
540-
throw new UserError("Namespace is required when table is specified", {
541-
telemetryMessage:
542-
"r2 catalog snapshot expiration disable missing namespace",
543-
});
544-
}
537+
validateNamespaceAndTable(
538+
args.namespace,
539+
args.table,
540+
args.bucket,
541+
"snapshot-expiration disable"
542+
);
545543

546544
if (args.namespace && args.table) {
547545
// Table-level snapshot expiration

‎packages/wrangler/src/r2/lifecycle.ts‎

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -165,9 +165,12 @@ export const r2BucketLifecycleAddCommand = createCommand({
165165
}
166166

167167
if (!name) {
168-
throw new UserError("Must specify a rule name.", {
169-
telemetryMessage: "r2 lifecycle add missing rule name",
170-
});
168+
throw new UserError(
169+
"Missing required rule name. Provide a name with --name <rule-name> or as a positional argument:\n wrangler r2 bucket lifecycle add <bucket> <name>",
170+
{
171+
telemetryMessage: "r2 lifecycle add missing rule name",
172+
}
173+
);
171174
}
172175

173176
const newRule: LifecycleRule = {
@@ -212,27 +215,35 @@ export const r2BucketLifecycleAddCommand = createCommand({
212215
}
213216

214217
if (selectedActions.length === 0) {
215-
throw new UserError("Must specify at least one action.", {
216-
telemetryMessage: "r2 lifecycle add missing action",
217-
});
218+
throw new UserError(
219+
"No lifecycle action specified. Use at least one of --expire-days, --expire-date, --ia-transition-days, --ia-transition-date, or --abort-multipart-days.",
220+
{
221+
telemetryMessage: "r2 lifecycle add missing action",
222+
}
223+
);
218224
}
219225

220226
for (const action of selectedActions) {
221227
let conditionType: "Age" | "Date";
222228
let conditionValue: number | string;
223229

224230
if (action === "abort-multipart") {
231+
let conditionValueFrom: "args" | "prompt" = "args";
225232
if (abortMultipartDays !== undefined) {
226233
conditionValue = abortMultipartDays;
227234
} else {
228235
conditionValue = await prompt(
229236
`Enter the number of days after which to ${formatActionDescription(action)}`
230237
);
238+
conditionValueFrom = "prompt";
231239
}
232240
if (!isNonNegativeNumber(String(conditionValue))) {
233-
throw new UserError("Must be a positive number.", {
234-
telemetryMessage: "r2 lifecycle add invalid abort multipart days",
235-
});
241+
throw new UserError(
242+
`The number of days ${conditionValueFrom === "args" ? "passed to --abort-multipart-days" : "for aborting incomplete multipart uploads"} must be a positive number (e.g. 7), but received '${String(conditionValue)}'.`,
243+
{
244+
telemetryMessage: "r2 lifecycle add invalid abort multipart days",
245+
}
246+
);
236247
}
237248

238249
conditionType = "Age";
@@ -266,7 +277,7 @@ export const r2BucketLifecycleAddCommand = createCommand({
266277
!isValidDate(String(conditionValue))
267278
) {
268279
throw new UserError(
269-
"Must be a positive number or a valid date in the YYYY-MM-DD format.",
280+
`Invalid value '${String(conditionValue)}' for ${action === "expire" ? "expiration" : "transition"} condition. Provide a positive number of days or a date in YYYY-MM-DD format (e.g. 30 or 2025-12-31).`,
270281
{
271282
telemetryMessage: "r2 lifecycle add invalid action condition",
272283
}
@@ -282,9 +293,12 @@ export const r2BucketLifecycleAddCommand = createCommand({
282293
const date = new Date(`${conditionValue}T00:00:00.000Z`);
283294
conditionValue = date.toISOString();
284295
} else {
285-
throw new UserError("Invalid condition input.", {
286-
telemetryMessage: "r2 lifecycle add invalid condition input",
287-
});
296+
throw new UserError(
297+
`Invalid value '${String(conditionValue)}' for ${action === "expire" ? "expiration" : "transition"} condition. Expected a positive number of days or a date in YYYY-MM-DD format (e.g. --expire-days 30 or --expire-date 2025-12-31).`,
298+
{
299+
telemetryMessage: "r2 lifecycle add invalid condition input",
300+
}
301+
);
288302
}
289303

290304
if (action === "expire") {

‎packages/wrangler/src/r2/lock.ts‎

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,12 @@ export const r2BucketLockAddCommand = createCommand({
145145
}
146146

147147
if (!name) {
148-
throw new UserError("Must specify a rule name.", {
149-
telemetryMessage: "r2 lock add missing rule name",
150-
});
148+
throw new UserError(
149+
"Missing required rule name. Provide a name with --name <rule-name> or as a positional argument:\n wrangler r2 bucket lock add <bucket> <name>",
150+
{
151+
telemetryMessage: "r2 lock add missing rule name",
152+
}
153+
);
151154
}
152155

153156
const newRule: BucketLockRule = {
@@ -204,16 +207,19 @@ export const r2BucketLockAddCommand = createCommand({
204207
};
205208
} else {
206209
throw new UserError(
207-
`Days must be a positive number: ${retentionDays}`,
210+
`The value for --retention-days must be a positive number, but received '${retentionDays}'.`,
208211
{
209212
telemetryMessage: "Retention days not a positive number.",
210213
}
211214
);
212215
}
213216
} else {
214-
throw new UserError(`Days must be a number.`, {
215-
telemetryMessage: "Retention days not a positive number.",
216-
});
217+
throw new UserError(
218+
"The value for --retention-days must be a number. Retry with a numeric value (e.g. --retention-days 30).",
219+
{
220+
telemetryMessage: "Retention days not a positive number.",
221+
}
222+
);
217223
}
218224
} else if (retentionDate !== undefined) {
219225
if (isValidDate(retentionDate)) {
@@ -225,7 +231,7 @@ export const r2BucketLockAddCommand = createCommand({
225231
};
226232
} else {
227233
throw new UserError(
228-
`Date must be a valid date in the YYYY-MM-DD format: ${String(retentionDate)}`,
234+
`The value for --retention-date must be in YYYY-MM-DD format (e.g. 2025-12-31), but received '${String(retentionDate)}'.`,
229235
{
230236
telemetryMessage:
231237
"Retention date not a valid date in the YYYY-MM-DD format.",
@@ -240,9 +246,12 @@ export const r2BucketLockAddCommand = createCommand({
240246
type: "Indefinite",
241247
};
242248
} else {
243-
throw new UserError(`Retention must be specified.`, {
244-
telemetryMessage: "Lock retention not specified.",
245-
});
249+
throw new UserError(
250+
"No retention specified. Use one of --retention-days <number>, --retention-date <YYYY-MM-DD>, or --retention-indefinite to set a retention.",
251+
{
252+
telemetryMessage: "Lock retention not specified.",
253+
}
254+
);
246255
}
247256
rules.push(newRule);
248257
logger.log(`Adding lock rule '${name}' to bucket '${bucket}'...`);

‎packages/wrangler/src/r2/sql.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,12 @@ export const r2SqlQueryCommand = createCommand({
117117

118118
const splitIndex = warehouse.indexOf("_");
119119
if (splitIndex === -1) {
120-
throw new UserError("Warehouse name has invalid format", {
121-
telemetryMessage: "r2 sql query invalid warehouse format",
122-
});
120+
throw new UserError(
121+
`Invalid warehouse name format '${warehouse}'. Expected the format '<account-id>_<bucket-name>' (e.g. 'abc123_my-bucket'). You can find the warehouse name by running: wrangler r2 bucket catalog get <bucket>`,
122+
{
123+
telemetryMessage: "r2 sql query invalid warehouse format",
124+
}
125+
);
123126
}
124127
const [accountId, bucketName] = [
125128
warehouse.slice(0, splitIndex),

0 commit comments

Comments
 (0)