fix asset upload rendering errors

This commit is contained in:
Jeff-Emmett 2025-03-19 18:30:15 -07:00
parent b11aecffa4
commit 2a3b79df15
2 changed files with 127 additions and 78 deletions

View File

@ -10,15 +10,17 @@ function getAssetObjectName(uploadId: string) {
// when a user uploads an asset, we store it in the bucket. we only allow image and video assets. // when a user uploads an asset, we store it in the bucket. we only allow image and video assets.
export async function handleAssetUpload(request: IRequest, env: Environment) { export async function handleAssetUpload(request: IRequest, env: Environment) {
// If this is a preflight request, return appropriate CORS headers // Add CORS headers that will be used for both success and error responses
const corsHeaders = {
'access-control-allow-origin': '*',
'access-control-allow-methods': 'GET, POST, HEAD, OPTIONS',
'access-control-allow-headers': '*',
'access-control-max-age': '86400',
}
// Handle preflight
if (request.method === 'OPTIONS') { if (request.method === 'OPTIONS') {
const headers = new Headers({ return new Response(null, { headers: corsHeaders })
'access-control-allow-origin': '*',
'access-control-allow-methods': 'POST, OPTIONS',
'access-control-allow-headers': 'content-type',
'access-control-max-age': '86400',
})
return new Response(null, { headers })
} }
try { try {
@ -26,21 +28,38 @@ export async function handleAssetUpload(request: IRequest, env: Environment) {
const contentType = request.headers.get('content-type') ?? '' const contentType = request.headers.get('content-type') ?? ''
if (!contentType.startsWith('image/') && !contentType.startsWith('video/')) { if (!contentType.startsWith('image/') && !contentType.startsWith('video/')) {
return error(400, 'Invalid content type') return new Response('Invalid content type', {
status: 400,
headers: corsHeaders
})
} }
if (await env.TLDRAW_BUCKET.head(objectName)) { if (await env.TLDRAW_BUCKET.head(objectName)) {
return error(409, 'Upload already exists') return new Response('Upload already exists', {
status: 409,
headers: corsHeaders
})
} }
await env.TLDRAW_BUCKET.put(objectName, request.body, { await env.TLDRAW_BUCKET.put(objectName, request.body, {
httpMetadata: request.headers, httpMetadata: request.headers,
}) })
return { ok: true } return new Response(JSON.stringify({ ok: true }), {
headers: {
...corsHeaders,
'content-type': 'application/json'
}
})
} catch (error) { } catch (error) {
console.error('Asset upload failed:', error); console.error('Asset upload failed:', error)
return new Response(`Upload failed: ${(error as Error).message}`, { status: 500 }); return new Response(JSON.stringify({ error: (error as Error).message }), {
status: 500,
headers: {
...corsHeaders,
'content-type': 'application/json'
}
})
} }
} }
@ -50,75 +69,103 @@ export async function handleAssetDownload(
env: Environment, env: Environment,
ctx: ExecutionContext ctx: ExecutionContext
) { ) {
const objectName = getAssetObjectName(request.params.uploadId) // Define CORS headers to be used consistently
const corsHeaders = {
// if we have a cached response for this request (automatically handling ranges etc.), return it 'access-control-allow-origin': '*',
const cacheKey = new Request(request.url, { headers: request.headers }) 'access-control-allow-methods': 'GET, HEAD, OPTIONS',
// @ts-ignore 'access-control-allow-headers': '*',
const cachedResponse = await caches.default.match(cacheKey) 'access-control-expose-headers': 'content-length, content-range',
if (cachedResponse) { 'access-control-max-age': '86400',
return cachedResponse
} }
// if not, we try to fetch the asset from the bucket // Handle preflight
const object = await env.TLDRAW_BUCKET.get(objectName, { if (request.method === 'OPTIONS') {
range: request.headers, return new Response(null, { headers: corsHeaders })
onlyIf: request.headers,
})
if (!object) {
return error(404)
} }
// write the relevant metadata to the response headers try {
const headers = new Headers() const objectName = getAssetObjectName(request.params.uploadId)
object.writeHttpMetadata(headers)
// assets are immutable, so we can cache them basically forever: // Handle cached response
headers.set('cache-control', 'public, max-age=31536000, immutable') const cacheKey = new Request(request.url, { headers: request.headers })
headers.set('etag', object.httpEtag) // @ts-ignore
const cachedResponse = await caches.default.match(cacheKey)
if (cachedResponse) {
const headers = new Headers(cachedResponse.headers)
Object.entries(corsHeaders).forEach(([key, value]) => headers.set(key, value))
return new Response(cachedResponse.body, {
status: cachedResponse.status,
headers
})
}
// Set comprehensive CORS headers for asset access // Get from bucket
headers.set('access-control-allow-origin', '*') const object = await env.TLDRAW_BUCKET.get(objectName, {
headers.set('access-control-allow-methods', 'GET, HEAD, OPTIONS') range: request.headers,
headers.set('access-control-allow-headers', '*') onlyIf: request.headers,
headers.set('access-control-expose-headers', 'content-length, content-range') })
headers.set('cross-origin-resource-policy', 'cross-origin')
headers.set('cross-origin-opener-policy', 'same-origin')
headers.set('cross-origin-embedder-policy', 'require-corp')
// cloudflare doesn't set the content-range header automatically in writeHttpMetadata, so we if (!object) {
// need to do it ourselves. return new Response('Not Found', {
let contentRange status: 404,
if (object.range) { headers: corsHeaders
if ('suffix' in object.range) { })
const start = object.size - object.range.suffix }
const end = object.size - 1
contentRange = `bytes ${start}-${end}/${object.size}` // Set up response headers
} else { const headers = new Headers()
const start = object.range.offset ?? 0 object.writeHttpMetadata(headers)
const end = object.range.length ? start + object.range.length - 1 : object.size - 1 Object.entries(corsHeaders).forEach(([key, value]) => headers.set(key, value))
if (start !== 0 || end !== object.size - 1) {
headers.set('cache-control', 'public, max-age=31536000, immutable')
headers.set('etag', object.httpEtag)
headers.set('cross-origin-resource-policy', 'cross-origin')
headers.set('cross-origin-opener-policy', 'same-origin')
headers.set('cross-origin-embedder-policy', 'require-corp')
// Handle content range
let contentRange
if (object.range) {
if ('suffix' in object.range) {
const start = object.size - object.range.suffix
const end = object.size - 1
contentRange = `bytes ${start}-${end}/${object.size}` contentRange = `bytes ${start}-${end}/${object.size}`
} else {
const start = object.range.offset ?? 0
const end = object.range.length ? start + object.range.length - 1 : object.size - 1
if (start !== 0 || end !== object.size - 1) {
contentRange = `bytes ${start}-${end}/${object.size}`
}
} }
} }
if (contentRange) {
headers.set('content-range', contentRange)
}
const body = 'body' in object && object.body ? object.body : null
const status = body ? (contentRange ? 206 : 200) : 304
// Cache successful responses
if (status === 200) {
const [cacheBody, responseBody] = body!.tee()
// @ts-ignore
ctx.waitUntil(caches.default.put(cacheKey, new Response(cacheBody, { headers, status })))
return new Response(responseBody, { headers, status })
}
return new Response(body, { headers, status })
} catch (error) {
console.error('Asset download failed:', error)
return new Response(
JSON.stringify({ error: (error as Error).message }),
{
status: 500,
headers: {
...corsHeaders,
'content-type': 'application/json'
}
}
)
} }
if (contentRange) {
headers.set('content-range', contentRange)
}
// make sure we get the correct body/status for the response
const body = 'body' in object && object.body ? object.body : null
const status = body ? (contentRange ? 206 : 200) : 304
// we only cache complete (200) responses
if (status === 200) {
const [cacheBody, responseBody] = body!.tee()
// @ts-ignore
ctx.waitUntil(caches.default.put(cacheKey, new Response(cacheBody, { headers, status })))
return new Response(responseBody, { headers, status })
}
return new Response(body, { headers, status })
} }

View File

@ -37,18 +37,19 @@ const { preflight, corsify } = cors({
return origin return origin
} }
// For development - check if it's a localhost or local IP // For development - check if it's a localhost or local IP (both http and https)
if ( if (
origin.match( origin.match(
/^http:\/\/(localhost|127\.0\.0\.1|192\.168\.|169\.254\.|10\.)/, /^https?:\/\/(localhost|127\.0\.0\.1|192\.168\.|169\.254\.|10\.)/,
) )
) { ) {
return origin return origin
} }
return undefined // If no match found, return * to allow all origins
return "*"
}, },
allowMethods: ["GET", "POST", "OPTIONS", "UPGRADE"], allowMethods: ["GET", "POST", "HEAD", "OPTIONS", "UPGRADE"],
allowHeaders: [ allowHeaders: [
"Content-Type", "Content-Type",
"Authorization", "Authorization",
@ -62,7 +63,8 @@ const { preflight, corsify } = cors({
"Content-Range", "Content-Range",
"Range", "Range",
"If-None-Match", "If-None-Match",
"If-Modified-Since" "If-Modified-Since",
"*"
], ],
maxAge: 86400, maxAge: 86400,
credentials: true, credentials: true,