Skip to content

Commit 06ddecb

Browse files
G4brymclaude
andauthored
Fix unhandled JSON.parse errors in upload metadata parameters (#149)
Wrap base64 + JSON decoding of customMetadata and httpMetadata query parameters in try-catch blocks for PutObject and CreateUpload endpoints. Previously, malformed input would crash with an unhandled exception returning a 500 error. Now returns a descriptive 400 error instead. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 2b87d1d commit 06ddecb

5 files changed

Lines changed: 134 additions & 12 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"r2-explorer": patch
3+
---
4+
5+
Return 400 instead of 500 when upload endpoints receive malformed base64-encoded metadata in `customMetadata` or `httpMetadata` query parameters

packages/worker/src/modules/buckets/multipart/createUpload.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { OpenAPIRoute } from "chanfana";
2+
import { HTTPException } from "hono/http-exception";
23
import { z } from "zod";
34
import type { AppContext } from "../../../types";
45

@@ -47,16 +48,28 @@ export class CreateUpload extends OpenAPIRoute {
4748

4849
let customMetadata = undefined;
4950
if (data.query.customMetadata) {
50-
customMetadata = JSON.parse(
51-
decodeURIComponent(escape(atob(data.query.customMetadata))),
52-
);
51+
try {
52+
customMetadata = JSON.parse(
53+
decodeURIComponent(escape(atob(data.query.customMetadata))),
54+
);
55+
} catch {
56+
throw new HTTPException(400, {
57+
message: "Invalid customMetadata: expected base64-encoded JSON",
58+
});
59+
}
5360
}
5461

5562
let httpMetadata = undefined;
5663
if (data.query.httpMetadata) {
57-
httpMetadata = JSON.parse(
58-
decodeURIComponent(escape(atob(data.query.httpMetadata))),
59-
);
64+
try {
65+
httpMetadata = JSON.parse(
66+
decodeURIComponent(escape(atob(data.query.httpMetadata))),
67+
);
68+
} catch {
69+
throw new HTTPException(400, {
70+
message: "Invalid httpMetadata: expected base64-encoded JSON",
71+
});
72+
}
6073
}
6174

6275
return await bucket.createMultipartUpload(key, {

packages/worker/src/modules/buckets/putObject.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,28 @@ export class PutObject extends OpenAPIRoute {
5454

5555
let customMetadata = undefined;
5656
if (data.query.customMetadata) {
57-
customMetadata = JSON.parse(
58-
decodeURIComponent(escape(atob(data.query.customMetadata))),
59-
);
57+
try {
58+
customMetadata = JSON.parse(
59+
decodeURIComponent(escape(atob(data.query.customMetadata))),
60+
);
61+
} catch {
62+
throw new HTTPException(400, {
63+
message: "Invalid customMetadata: expected base64-encoded JSON",
64+
});
65+
}
6066
}
6167

6268
let httpMetadata = undefined;
6369
if (data.query.httpMetadata) {
64-
httpMetadata = JSON.parse(
65-
decodeURIComponent(escape(atob(data.query.httpMetadata))),
66-
);
70+
try {
71+
httpMetadata = JSON.parse(
72+
decodeURIComponent(escape(atob(data.query.httpMetadata))),
73+
);
74+
} catch {
75+
throw new HTTPException(400, {
76+
message: "Invalid httpMetadata: expected base64-encoded JSON",
77+
});
78+
}
6779
}
6880

6981
return await bucket.put(key, c.req.raw.body, {

packages/worker/tests/integration/buckets.test.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,56 @@ describe("Bucket Endpoints", () => {
268268
expect(await r2Object?.text()).toBe(newObjectContent);
269269
});
270270

271+
it("should return 400 for malformed customMetadata", async () => {
272+
if (!MY_TEST_BUCKET_1)
273+
throw new Error("MY_TEST_BUCKET_1 binding not available");
274+
275+
const objectKey = "bad-metadata.txt";
276+
const base64ObjectKey = btoa(objectKey);
277+
const blobBody = new Blob(["content"], {
278+
type: "application/octet-stream",
279+
});
280+
281+
const badCustomMetadata = btoa("not-valid-json");
282+
283+
const request = createTestRequest(
284+
`/api/buckets/MY_TEST_BUCKET_1/upload?key=${encodeURIComponent(base64ObjectKey)}&customMetadata=${encodeURIComponent(badCustomMetadata)}`,
285+
"POST",
286+
blobBody,
287+
{ "Content-Type": "application/octet-stream" },
288+
);
289+
290+
const response = await app.fetch(request, env, createExecutionContext());
291+
expect(response.status).toBe(400);
292+
const body = await response.text();
293+
expect(body).toContain("Invalid customMetadata");
294+
});
295+
296+
it("should return 400 for malformed httpMetadata", async () => {
297+
if (!MY_TEST_BUCKET_1)
298+
throw new Error("MY_TEST_BUCKET_1 binding not available");
299+
300+
const objectKey = "bad-metadata.txt";
301+
const base64ObjectKey = btoa(objectKey);
302+
const blobBody = new Blob(["content"], {
303+
type: "application/octet-stream",
304+
});
305+
306+
const badHttpMetadata = btoa("{broken json");
307+
308+
const request = createTestRequest(
309+
`/api/buckets/MY_TEST_BUCKET_1/upload?key=${encodeURIComponent(base64ObjectKey)}&httpMetadata=${encodeURIComponent(badHttpMetadata)}`,
310+
"POST",
311+
blobBody,
312+
{ "Content-Type": "application/octet-stream" },
313+
);
314+
315+
const response = await app.fetch(request, env, createExecutionContext());
316+
expect(response.status).toBe(400);
317+
const body = await response.text();
318+
expect(body).toContain("Invalid httpMetadata");
319+
});
320+
271321
it("POST /api/buckets/NON_EXISTENT_BUCKET/upload - should return 500 if bucket binding does not exist", async () => {
272322
const base64ObjectKey = btoa("test.txt");
273323
const blobBody = new Blob(["content"], {

packages/worker/tests/integration/multipart.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,48 @@ describe("Multipart Upload Endpoints", () => {
107107
const response = await app.fetch(request, env, createExecutionContext());
108108
expect(response.status).toBe(400);
109109
});
110+
111+
it("should return 400 for malformed customMetadata", async () => {
112+
if (!MY_TEST_BUCKET_1)
113+
throw new Error("MY_TEST_BUCKET_1 not available");
114+
115+
const objectKey = "bad-metadata.dat";
116+
const base64ObjectKey = btoa(objectKey);
117+
const badCustomMetadata = btoa("not-valid-json");
118+
119+
const request = createTestRequest(
120+
`/api/buckets/${BUCKET_NAME}/multipart/create?key=${encodeURIComponent(base64ObjectKey)}&customMetadata=${encodeURIComponent(badCustomMetadata)}`,
121+
"POST",
122+
undefined,
123+
{ "Content-Type": "application/json" },
124+
);
125+
126+
const response = await app.fetch(request, env, createExecutionContext());
127+
expect(response.status).toBe(400);
128+
const body = await response.text();
129+
expect(body).toContain("Invalid customMetadata");
130+
});
131+
132+
it("should return 400 for malformed httpMetadata", async () => {
133+
if (!MY_TEST_BUCKET_1)
134+
throw new Error("MY_TEST_BUCKET_1 not available");
135+
136+
const objectKey = "bad-metadata.dat";
137+
const base64ObjectKey = btoa(objectKey);
138+
const badHttpMetadata = btoa("{broken json");
139+
140+
const request = createTestRequest(
141+
`/api/buckets/${BUCKET_NAME}/multipart/create?key=${encodeURIComponent(base64ObjectKey)}&httpMetadata=${encodeURIComponent(badHttpMetadata)}`,
142+
"POST",
143+
undefined,
144+
{ "Content-Type": "application/json" },
145+
);
146+
147+
const response = await app.fetch(request, env, createExecutionContext());
148+
expect(response.status).toBe(400);
149+
const body = await response.text();
150+
expect(body).toContain("Invalid httpMetadata");
151+
});
110152
});
111153

112154
describe("PartUpload (POST /api/buckets/:bucket/multipart/upload)", () => {

0 commit comments

Comments
 (0)