Create the POST Endpoint

The POST endpoint manages multipart uploads through two key operations: initializing new uploads and finalizing completed ones. The action query parameter determines which operation to perform.

Be sure to include the locals parameter in the POST endpoint to access your environment variables and CLOUD_FILES binding.

Setup CORS handling

Since multipart uploads require direct communication with the worker domain (not your Webflow Cloud domain), you need to set up proper CORS configuration. The imported API utility automatically handles CORS headers and preflight requests to ensure seamless cross-origin communication.

Access your Object Storage bucket

Retrieve the CLOUD_FILES binding that provides access to your Object Storage bucket. This binding contains all the multipart upload methods you'll need throughout the upload process.

Get Action Parameter

Parse the action query parameter to route between different multipart upload operations. The POST endpoint supports both create and complete actions.

Initialize multipart upload session

When action=create, this code establishes a new multipart upload session in Object Storage. The browser provides the key and contentType, then receives a unique uploadId that identifies the entire upload process.

Finalize multipart upload

Once all parts are uploaded through the PUT endpoint, the browser will send a request to this POST endpoint with the action parameter set to complete with an ordered list of all uploaded parts with their corresponding entity tags (ETags).

When action=complete, this endpoint consolidates all uploaded parts into a single file in Object Storage using multipartUpload.complete() method

multipart.ts
ExpandClose
1
import type { APIRoute } from "astro";
2
import { API } from "../../utils/api";
3

4
interface MultipartUploadRequest {
5
key: string;
6
contentType?: string;
7
}
8

9
interface CompleteMultipartRequest {
10
uploadId: string;
11
key: string;
12
parts: R2UploadedPart[];
13
}
14

15
// Creates and completes a new multipart upload session
16
export const POST: APIRoute = async ({ request, locals }) => {
17
// Set the origin for the API
18
API.init(locals.runtime.env.ORIGIN);
19

20
// Handle CORS preflight requests
21
if (request.method === "OPTIONS") {
22
console.log("CORS preflight request from:", request.headers.get("Origin"));
23
return API.cors(request);
24
}
25

26
try {
27
// Check if bucket is available
28
const bucket = locals.runtime.env.CLOUD_FILES;
29
if (!bucket) {
30
return API.error("Cloud storage not configured", request, 500);
31
}
32

33
const url = new URL(request.url);
34
const action = url.searchParams.get("action");
35

36
if (!action) {
37
return API.error("Missing action parameter", request, 400);
38
}
39

40
switch (action) {
41
case "create": {
42
// Create a new multipart upload
43
const body: MultipartUploadRequest = await request.json();
44

45
if (!body.key) {
46
return API.error("Missing key parameter", request, 400);
47
}
48

49
try {
50
const multipartUpload = await bucket.createMultipartUpload(body.key, {
51
httpMetadata: body.contentType
52
? {
53
contentType: body.contentType,
54
}
55
: undefined,
56
});
57

58
return API.success(
59
{
60
success: true,
61
key: multipartUpload.key,
62
uploadId: multipartUpload.uploadId,
63
},
64
request
65
);
66
} catch (error) {
67
console.error("Failed to create multipart upload:", error);
68
return API.error("Failed to create multipart upload", request, 500);
69
}
70
}
71

72
case "complete": {
73
// Complete a multipart upload
74
const body: CompleteMultipartRequest = await request.json();
75

76
if (!body.uploadId || !body.key || !body.parts) {
77
return API.error("Missing required parameters", request, 400);
78
}
79

80
try {
81
const multipartUpload = bucket.resumeMultipartUpload(
82
body.key,
83
body.uploadId
84
);
85

86
// Parts are already in R2UploadedPart format
87
const r2Parts = body.parts;
88

89
const object = await multipartUpload.complete(r2Parts);
90

91
return API.success(
92
{
93
success: true,
94
key: object.key,
95
etag: object.httpEtag,
96
size: object.size,
97
},
98
request
99
);
100
} catch (error: any) {
101
console.error("Failed to complete multipart upload:", error);
102
return API.error(
103
error.message || "Failed to complete multipart upload",
104
request,
105
400
106
);
107
}
108
}
109

110
default:
111
return API.error(`Unknown action: ${action}`, request, 400);
112
}
113
} catch (error) {
114
console.error("Multipart upload error:", error);
115
return API.error("Multipart upload failed", request, 500);
116
}
117
};
118

119
// Uploads individual parts of a multipart upload
120
export const PUT: APIRoute = async ({ request, locals }) => {
121
// Set the origin for the API
122
API.init(locals.runtime.env.ORIGIN);
123

124
// Handle CORS preflight requests
125
if (request.method === "OPTIONS") {
126
console.log("CORS preflight request from:", request.headers.get("Origin"));
127
return API.cors(request);
128
}
129

130
try {
131
// Check if bucket is available
132
const bucket = locals.runtime.env.CLOUD_FILES;
133
if (!bucket) {
134
return API.error("Cloud storage not configured", request, 500);
135
}
136

137
const url = new URL(request.url);
138
const action = url.searchParams.get("action");
139

140
if (action !== "upload-part") {
141
return API.error(`Unknown action: ${action}`, request, 400);
142
}
143

144
const uploadId = url.searchParams.get("uploadId");
145
const partNumberStr = url.searchParams.get("partNumber");
146
const key = url.searchParams.get("key");
147

148
if (!uploadId || !partNumberStr || !key) {
149
return API.error("Missing uploadId, partNumber, or key", request, 400);
150
}
151

152
const partNumber = parseInt(partNumberStr);
153
if (isNaN(partNumber) || partNumber < 1) {
154
return API.error("Invalid part number", request, 400);
155
}
156

157
if (!request.body) {
158
return API.error("Missing request body", request, 400);
159
}
160

161
try {
162
const multipartUpload = bucket.resumeMultipartUpload(key, uploadId);
163

164
// Convert request body to ArrayBuffer to get known length
165
const arrayBuffer = await request.arrayBuffer();
166
const uploadedPart = await multipartUpload.uploadPart(
167
partNumber,
168
arrayBuffer
169
);
170

171
return API.success(
172
{
173
success: true,
174
partNumber: uploadedPart.partNumber,
175
etag: uploadedPart.etag,
176
},
177
request
178
);
179
} catch (error: any) {
180
console.error("Failed to upload part:", error);
181
return API.error(error.message || "Failed to upload part", request, 400);
182
}
183
} catch (error) {
184
console.error("Upload part error:", error);
185
return API.error("Upload part failed", request, 500);
186
}
187
};
188

189
// Aborts a multipart upload
190
export const DELETE: APIRoute = async ({ request, locals }) => {
191
// Set the origin for the API
192
API.init(locals.runtime.env.ORIGIN);
193

194
// Handle CORS preflight requests
195
if (request.method === "OPTIONS") {
196
console.log("CORS preflight request from:", request.headers.get("Origin"));
197
return API.cors(request);
198
}
199

200
try {
201
// Check if bucket is available
202
const bucket = locals.runtime.env.CLOUD_FILES;
203
if (!bucket) {
204
return API.error("Cloud storage not configured", request, 500);
205
}
206

207
const url = new URL(request.url);
208
const action = url.searchParams.get("action");
209

210
if (action !== "abort") {
211
return API.error(`Unknown action: ${action}`, request, 400);
212
}
213

214
const uploadId = url.searchParams.get("uploadId");
215
const key = url.searchParams.get("key");
216

217
if (!uploadId || !key) {
218
return API.error("Missing uploadId or key", request, 400);
219
}
220

221
try {
222
const multipartUpload = bucket.resumeMultipartUpload(key, uploadId);
223
await multipartUpload.abort();
224

225
return API.success(
226
{
227
success: true,
228
message: "Multipart upload aborted successfully",
229
},
230
request
231
);
232
} catch (error: any) {
233
console.error("Failed to abort multipart upload:", error);
234
return API.error(
235
error.message || "Failed to abort multipart upload",
236
request,
237
400
238
);
239
}
240
} catch (error) {
241
console.error("Abort multipart upload error:", error);
242
return API.error("Abort multipart upload failed", request, 500);
243
}
244
};
245

246
export const OPTIONS: APIRoute = async ({ request, locals }) => {
247
// Set the origin for the API
248
API.init(locals.runtime.env.ORIGIN);
249
return API.cors(request);
250
};