High-level Cloudflare R2 storage API wrapper for generating pre-signed URLs and performing object operations
Pre-signed URLs provide secure, time-limited access to R2 objects without exposing credentials. This is the recommended approach for client-side file uploads and downloads.
Core Capabilities:
Key Methods:
presignedUploadUrl(options: PresignedUploadUrlOptions): Promise<PresignedUrlResult> - Generate upload URLpresignedDownloadUrl(key: string, options?: PresignedDownloadUrlOptions): Promise<PresignedUrlResult> - Generate download URLKey Interfaces:
PresignedUploadUrlOptions - key: string, contentType: string, metadata?, expiresIn?, maxFileSize?, allowedContentTypes?PresignedDownloadUrlOptions - expiresIn?PresignedUrlResult - url: string, key: string, contentType: string, expiresIn: number, metadata?Default Behaviors:
Threading Model:
Lifecycle:
Common Patterns:
allowedContentTypes to restrict upload typesIntegration Points:
x-amz-meta- prefix (S3-compatible)Critical Edge Cases:
allowedContentTypes: ['image/*'] matches any image typeallowedContentTypes array allows multiple patternsx-amz-meta- prefix in upload request headersException Handling:
Creates a secure, time-limited URL that allows clients to upload files directly to R2 without exposing your credentials.
/**
* Generate a pre-signed URL for uploading an object
* @param options - Upload URL options including key, content type, and optional metadata
* @returns Pre-signed URL and metadata
*/
presignedUploadUrl(options: PresignedUploadUrlOptions): Promise<PresignedUrlResult>;
interface PresignedUploadUrlOptions {
/** Object key (filename) in the bucket */
key: string;
/** Content type (MIME type) of the file */
contentType: string;
/** Optional metadata headers to include */
metadata?: Record<string, string>;
/** Expiration time in seconds (default: 86400 = 24 hours) */
expiresIn?: number;
/** Maximum allowed file size in bytes (validation only, not enforced by R2) */
maxFileSize?: number;
/** Allowed content types (supports wildcards like 'image/*') */
allowedContentTypes?: string[];
}
interface PresignedUrlResult {
/** The pre-signed URL */
url: string;
/** Object key */
key: string;
/** Content type */
contentType: string;
/** Expiration time in seconds */
expiresIn: number;
/** Metadata that was included in the signature */
metadata?: Record<string, string>;
}Usage Examples:
import { R2Client } from '@cfkit/r2';
const r2 = new R2Client({
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!
});
const bucket = r2.bucket('gallery');
// Basic pre-signed upload URL
const result = await bucket.presignedUploadUrl({
key: 'photo.jpg',
contentType: 'image/jpeg',
expiresIn: 3600
});
// Client uploads using the pre-signed URL
await fetch(result.url, {
method: 'PUT',
headers: {
'Content-Type': 'image/jpeg'
},
body: file
});
// Pre-signed URL with metadata
const resultWithMeta = await bucket.presignedUploadUrl({
key: 'photo.jpg',
contentType: 'image/jpeg',
metadata: {
'original-filename': 'vacation-photo.jpg',
'uploaded-by': 'user-123'
},
expiresIn: 3600
});
// Client uploads with metadata headers
await fetch(resultWithMeta.url, {
method: 'PUT',
headers: {
'Content-Type': 'image/jpeg',
'x-amz-meta-original-filename': 'vacation-photo.jpg',
'x-amz-meta-uploaded-by': 'user-123'
},
body: file
});
// Pre-signed URL with content type validation
try {
const result = await bucket.presignedUploadUrl({
key: 'document.pdf',
contentType: 'application/pdf',
allowedContentTypes: ['image/*'] // Only images allowed
});
} catch (error) {
console.error(error.message);
// "Content type 'application/pdf' is not allowed. Allowed types: image/*"
}Creates a secure, time-limited URL that allows clients to download files directly from R2 without exposing your credentials.
/**
* Generate a pre-signed URL for downloading an object
* @param key - Object key (filename)
* @param options - Download URL options
* @returns Pre-signed URL and metadata
*/
presignedDownloadUrl(key: string, options?: PresignedDownloadUrlOptions): Promise<PresignedUrlResult>;
interface PresignedDownloadUrlOptions {
/** Expiration time in seconds (default: 3600 = 1 hour) */
expiresIn?: number;
}Usage Examples:
import { R2Client } from '@cfkit/r2';
const r2 = new R2Client({
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
accessKeyId: process.env.R2_ACCESS_KEY_ID!,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY!
});
const bucket = r2.bucket('gallery');
// Basic pre-signed download URL
const result = await bucket.presignedDownloadUrl('photo.jpg', {
expiresIn: 3600
});
console.log('Download URL:', result.url);
// Share this URL with users who need temporary access
// Check existence before generating download URL
const exists = await bucket.objectExists('photo.jpg');
if (exists) {
const downloadUrl = await bucket.presignedDownloadUrl('photo.jpg');
console.log('Download URL:', downloadUrl.url);
}
// Short expiration for sensitive files
const sensitiveUrl = await bucket.presignedDownloadUrl('private-doc.pdf', {
expiresIn: 300 // 5 minutes
});
// Long expiration for public assets
const publicUrl = await bucket.presignedDownloadUrl('public-image.jpg', {
expiresIn: 86400 * 7 // 7 days
});The allowedContentTypes option in presignedUploadUrl supports wildcard patterns:
// Allow only images
allowedContentTypes: ['image/*']
// Allow specific types
allowedContentTypes: ['image/jpeg', 'image/png', 'image/gif']
// Allow multiple type families
allowedContentTypes: ['image/*', 'video/*']
// Allow documents
allowedContentTypes: ['application/pdf', 'application/msword']Validation Rules:
image/* matches image/jpeg, image/png)Usage Examples:
// Restrict to images only
try {
const url = await bucket.presignedUploadUrl({
key: 'photo.jpg',
contentType: 'image/jpeg',
allowedContentTypes: ['image/*']
});
// Success: image/jpeg matches image/*
} catch (error) {
// Error if contentType doesn't match
}
// Multiple allowed types
const url = await bucket.presignedUploadUrl({
key: 'file.jpg',
contentType: 'image/jpeg',
allowedContentTypes: ['image/*', 'video/*', 'application/pdf']
});
// Reject non-matching type
try {
const url = await bucket.presignedUploadUrl({
key: 'file.pdf',
contentType: 'application/pdf',
allowedContentTypes: ['image/*'] // PDF not allowed
});
} catch (error) {
console.error('Content type not allowed');
}Metadata is stored with objects using x-amz-meta- prefixed headers. When using pre-signed URLs, ensure the client includes these headers exactly as specified:
// Server generates pre-signed URL with metadata
const result = await bucket.presignedUploadUrl({
key: 'file.jpg',
contentType: 'image/jpeg',
metadata: {
'original-filename': 'photo.jpg',
'uploaded-by': 'user-123'
}
});
// Client must include these headers when uploading
await fetch(result.url, {
method: 'PUT',
headers: {
'Content-Type': 'image/jpeg',
'x-amz-meta-original-filename': 'photo.jpg',
'x-amz-meta-uploaded-by': 'user-123'
},
body: file
});Metadata Rules:
x-amz-meta- prefixed headersgetObject()Important Notes:
getObject() or presignedDownloadUrl() doesn't include metadataFor direct client uploads using pre-signed URLs, configure your R2 bucket CORS policy. Cloudflare R2 requires explicit header names (wildcards are not supported):
{
"rules": [
{
"allowed": {
"methods": ["PUT", "GET", "POST", "DELETE"],
"origins": ["https://yourdomain.com"],
"headers": [
"content-type",
"x-amz-meta-original-filename",
"x-amz-meta-uploaded-at",
"x-amz-meta-uploaded-by"
]
},
"exposeHeaders": ["ETag"],
"maxAgeSeconds": 3000
}
]
}CORS Configuration Requirements:
PUT method for uploadsGET method for downloadscontent-type headerexposeHeaders can include ETag for upload verificationmaxAgeSeconds controls browser CORS preflight cacheCommon CORS Patterns:
// Allow all origins (development only)
{
"rules": [
{
"allowed": {
"methods": ["PUT", "GET"],
"origins": ["*"],
"headers": ["content-type", "x-amz-meta-*"]
}
}
]
}
// Production: specific origins
{
"rules": [
{
"allowed": {
"methods": ["PUT", "GET"],
"origins": [
"https://app.example.com",
"https://www.example.com"
],
"headers": [
"content-type",
"x-amz-meta-user-id",
"x-amz-meta-timestamp"
]
},
"exposeHeaders": ["ETag"],
"maxAgeSeconds": 3600
}
]
}URL generation may throw errors. Always wrap operations in try-catch blocks:
try {
const url = await bucket.presignedUploadUrl({
key: 'file.jpg',
contentType: 'image/jpeg',
expiresIn: 3600
});
console.log('URL generated:', url.url);
} catch (error) {
if (error instanceof Error) {
console.error('Failed to generate URL:', error.message);
}
}Error Handling Patterns:
// Content type validation error
try {
const url = await bucket.presignedUploadUrl({
key: 'file.pdf',
contentType: 'application/pdf',
allowedContentTypes: ['image/*']
});
} catch (error) {
if (error instanceof Error) {
// Error: Content type 'application/pdf' is not allowed
console.error('Validation failed:', error.message);
}
}
// Authentication error
try {
const url = await bucket.presignedDownloadUrl('photo.jpg');
} catch (error) {
if (error instanceof Error) {
// Error: Invalid credentials
console.error('Authentication failed:', error.message);
}
}
// Client-side URL usage errors
// When client uses expired URL:
fetch(expiredUrl, { method: 'PUT', body: file })
.then(response => {
if (response.status === 403) {
console.error('URL expired');
} else if (response.status === 404) {
console.error('Object not found');
}
});Common Error Scenarios:
allowedContentTypesInstall with Tessl CLI
npx tessl i tessl/npm-cfkit--r2