Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 27 additions & 2 deletions supabase/functions/_backend/files/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,20 @@ async function recoverUploadOffsetFromDurableObject(
}
}

function retryableUploadUnavailableResponse(): Response {
return new Response(JSON.stringify({
error: 'upload_retryable',
message: 'Upload worker moved during this request. Retry the upload request.',
}), {
status: 503,
headers: {
'Content-Type': 'application/json',
'Retry-After': '1',
'Tus-Resumable': TUS_VERSION,
},
})
}

async function fetchUploadHandlerWithRetry(
c: Context,
handler: DurableObjectStub,
Expand Down Expand Up @@ -201,9 +215,10 @@ async function fetchUploadHandlerWithRetry(
}
catch (error) {
lastError = error
const isRetryableDurableObjectError = isRetryableDurableObjectFetchError(error)
const shouldRetry = canRetryRequest
&& attempt < maxAttempts
&& isRetryableDurableObjectFetchError(error)
&& isRetryableDurableObjectError

cloudlogErr({
requestId: c.get('requestId'),
Expand All @@ -217,6 +232,15 @@ async function fetchUploadHandlerWithRetry(
})

if (!shouldRetry) {
if (!canRetryRequest && isRetryableDurableObjectError) {
cloudlog({
requestId: c.get('requestId'),
message: 'upload handler - durable object fetch failed for streaming request, returning retryable response',
attempt,
fileId: c.get('fileId'),
})
return retryableUploadUnavailableResponse()
}
throw error
}

Expand Down Expand Up @@ -378,7 +402,7 @@ async function getHandler(c: Context): Promise<Response> {
}
catch (error) {
cloudlogErr({ requestId: c.get('requestId'), message: 'getHandler files get failed', fileId, error })
throw quickError(503, 'upstream_unavailable', 'File storage temporarily unavailable', { fileId })
throw quickError(503, 'upstream_unavailable', 'File storage temporarily unavailable', { fileId }, error, { alert: false })
}
if (object == null) {
cloudlog({ requestId: c.get('requestId'), message: 'getHandler files object is null' })
Expand Down Expand Up @@ -878,4 +902,5 @@ app.route('/ok', ok)
export const filesTestUtils = {
fetchUploadHandlerWithRetry,
isRetryableDurableObjectFetchError,
retryableUploadUnavailableResponse,
}
18 changes: 14 additions & 4 deletions supabase/functions/_backend/public/build/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ interface BuilderJobResponse {
status: string
}

function throwBuilderUnavailable(message: string, moreInfo: Record<string, unknown> = {}, cause?: unknown): never {
throw quickError(503, 'service_unavailable', message, moreInfo, cause, { alert: false })
}

/**
* Construct the JSON body forwarded to the builder's POST /jobs endpoint.
* Extracted for testability — the handler calls this, and unit tests assert the shape.
Expand Down Expand Up @@ -171,7 +175,7 @@ export async function requestBuild(
builder_url_configured: !!builderUrl,
builder_api_key_configured: !!builderApiKey,
})
throw quickError(503, 'service_unavailable', 'Build service unavailable (builder not configured)')
throwBuilderUnavailable('Build service unavailable (builder not configured)')
}

try {
Expand Down Expand Up @@ -227,10 +231,16 @@ export async function requestBuild(
app_id,
platform,
})
throw quickError(503, 'service_unavailable', 'Build service unavailable (builder error)')
throwBuilderUnavailable('Build service unavailable (builder error)', {
status: builderResponse.status,
statusText: builderResponse.statusText,
})
}
}
catch (error) {
if (error && typeof error === 'object' && 'status' in error && (error as { status?: unknown }).status === 503) {
throw error
}
cloudlogErr({
requestId: c.get('requestId'),
message: 'Builder API fetch failed',
Expand All @@ -242,7 +252,7 @@ export async function requestBuild(
app_id,
platform,
})
throw quickError(503, 'service_unavailable', 'Build service unavailable (builder call failed)')
throwBuilderUnavailable('Build service unavailable (builder call failed)', {}, error)
}

const upload_expires_at = new Date(Date.now() + 60 * 60 * 1000)
Expand All @@ -255,7 +265,7 @@ export async function requestBuild(
builder_url: builderUrl,
builder_api_key_present: !!builderApiKey,
})
throw quickError(503, 'service_unavailable', 'Build service unavailable (upload URL missing)')
throwBuilderUnavailable('Build service unavailable (upload URL missing)')
}

const upload_url = `${getEnv(c, 'PUBLIC_URL') || 'https://api.capgo.app'}/build/upload/${builderJob.jobId}`
Expand Down
Loading
Loading