fix(tandoor): use File constructor for proper multipart uploads

- Remove unreliable URL pass-through strategy (image_url field)
- Always download and upload images as File objects
- Get MIME type from HTTP response headers for URLs
- Use File constructor (not just Blob) for proper multipart metadata
- Add comprehensive error logging with headers and file metadata
- Simplify to single reliable upload path

Fixes 400 'Upload a valid image' error caused by Blob not providing
proper filename/MIME metadata in multipart form data.
This commit is contained in:
Giancarmine Salucci
2025-12-21 05:19:33 +01:00
parent 856c5c26f4
commit cc7b8032cb
4 changed files with 16022 additions and 76 deletions

15468
docs/Tandoor (2.3.6).yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,485 @@
# Execution Plan: Fix Tandoor Image Upload (v2)
**Date:** 2025-12-21
**Author:** Analyst Agent
**Status:** Draft
**Issue:** URL pass-through fails with 500, file upload fails with 400 "Upload a valid image"
## Problem Statement
Thumbnail upload to Tandoor is failing with two distinct errors:
```
[Tandoor Upload] Using URL pass-through strategy
[Tandoor Upload] URL pass-through failed (500), trying file upload
[Tandoor Upload] Using fallback fetch strategy
[Tandoor Upload] Failed: 400 Bad Request
[Tandoor Upload] Response: {"image":["Upload a valid image. The file you uploaded was either not an image or a corrupted image."]}
```
## Root Cause Analysis
### Issue 1: URL Pass-through Fails (500 Error)
**Current Implementation:**
```typescript
formData.append('image_url', imageUrl);
```
**Problem:** The OpenAPI spec shows that `RecipeImage` schema has two fields:
- `image`: `type: string, format: uri` (for file upload)
- `image_url`: `type: string, maxLength: 4096` (for URL)
However, the **500 error** suggests Tandoor might not support `image_url` field in this version, or it's encountering an error when trying to fetch the URL server-side.
### Issue 2: File Upload Fails (400 Error)
**Current Implementation:**
```typescript
const blob = new Blob([imageBuffer], { type: parsed.mimeType });
formData.append('image', blob, `recipe-image${extension}`);
```
**Problem:** According to OpenAPI spec:
```yaml
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/RecipeImage'
```
The `image` field expects `format: uri` which in multipart context means an **actual file with proper headers**. Our current Blob might be missing critical multipart headers or the blob isn't being properly recognized as a file.
**Root Cause:** In Node.js/server-side context, `Blob` API might not work the same as in browser. We need to use proper Node.js file handling or ensure the Blob is correctly formatted for multipart upload.
## Analysis from OpenAPI Spec
### Endpoint Definition
```yaml
/api/recipe/{id}/image/:
put:
operationId: apiRecipeImageUpdate
requestBody:
content:
multipart/form-data:
schema:
$ref: '#/components/schemas/RecipeImage'
```
### RecipeImage Schema
```yaml
RecipeImage:
type: object
properties:
image:
type: string
format: uri
nullable: true
image_url:
type: string
nullable: true
maxLength: 4096
```
### Key Insights
1. **Both fields are optional** (`nullable: true`)
2. **`image_url` exists** but may not be working (500 error suggests server-side issue)
3. **`image` expects file upload** via multipart/form-data
4. **No Content-Type header** should be set manually (let browser/Node set it for multipart)
## Proposed Solution
### Strategy Change
Since `image_url` is causing 500 errors (Tandoor server can't fetch or process the URL), we should:
1. **Always download and upload the image** (more reliable)
2. **Fix the file upload format** to ensure proper multipart headers
3. **Remove URL pass-through** (or make it optional/fallback)
### Technical Fix Required
The issue is that in **server-side Node.js context** (SvelteKit server), the `Blob` API doesn't create proper multipart form data. We need to:
1. Use `File` constructor with proper filename and type
2. Or use `Buffer` with proper form-data library
3. Ensure proper MIME type is set
4. Let FormData handle Content-Type header (don't set it manually)
## Implementation Plan
### Story 1: Fix File Upload for Direct URLs
**Objective:** Make direct URL images download and upload correctly
**Current Problem:**
```typescript
const response = await fetch(imageUrl);
const imageBlob = await response.blob();
formData.append('image', imageBlob, `recipe-image${extension}`);
// Fails with 400: "Upload a valid image"
```
**Solution:**
In SvelteKit server environment, we need to handle this differently:
```typescript
// Download the image
const response = await fetch(imageUrl);
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
// Get proper MIME type
const mimeType = response.headers.get('content-type') || 'image/jpeg';
const extension = getExtensionFromMimeType(mimeType);
// Create a proper File object (if available) or use Blob correctly
const blob = new Blob([buffer], { type: mimeType });
const file = new File([blob], `recipe-image${extension}`, { type: mimeType });
const formData = new FormData();
formData.append('image', file);
```
**Acceptance Criteria:**
- Direct URL images (from meta tags) upload successfully
- No 400 "Upload a valid image" errors
- Proper MIME type detected from response headers
- File has correct extension and name
---
### Story 2: Fix Base64 Data URL Upload
**Objective:** Make base64 screenshot images upload correctly
**Current Problem:**
```typescript
const imageBuffer = Buffer.from(parsed.base64Data, 'base64');
const blob = new Blob([imageBuffer], { type: parsed.mimeType });
formData.append('image', blob, `recipe-image${extension}`);
// Fails with 400: "Upload a valid image"
```
**Solution:**
```typescript
// Parse base64 data URL
const parsed = parseDataUrl(imageUrl);
const imageBuffer = Buffer.from(parsed.base64Data, 'base64');
const extension = getExtensionFromMimeType(parsed.mimeType);
// Create proper File object
const blob = new Blob([imageBuffer], { type: parsed.mimeType });
const file = new File([blob], `recipe-image${extension}`, { type: parsed.mimeType });
const formData = new FormData();
formData.append('image', file);
```
**Key Change:** Use `File` constructor instead of just `Blob`
**Acceptance Criteria:**
- Base64 images (screenshots) upload successfully
- Proper MIME type from data URL is preserved
- File has correct extension
---
### Story 3: Remove or Fix URL Pass-through Strategy
**Objective:** Handle the 500 error from `image_url` field
**Options:**
**Option A: Remove URL Pass-through**
- Always download and upload images
- More reliable, works around Tandoor server issue
- Slightly more bandwidth usage
**Option B: Make URL Pass-through Optional**
- Try `image_url` first
- On 500 error, fall back to file upload immediately
- Keep current behavior but with better error handling
**Recommendation:** **Option A** - Remove URL pass-through for now since:
1. It's causing 500 errors
2. File upload is more reliable
3. Performance difference is minimal
4. Simpler code (one path instead of multiple fallbacks)
**If keeping URL pass-through**, improve error handling:
```typescript
// Try URL pass-through
const urlResult = await tryUrlPassthrough(recipeId, imageUrl, token);
if (urlResult.success) {
return urlResult;
}
// On ANY error (500, 400, etc.), fall back to file upload
console.warn(`URL pass-through failed (${urlResult.status}), using file upload`);
return uploadAsFile(recipeId, imageUrl, token);
```
**Acceptance Criteria:**
- No 500 errors in logs
- Clear decision: either URL pass-through works or it's removed
- Fallback to file upload is automatic
---
### Story 4: Ensure Proper FormData Headers
**Objective:** Let FormData handle Content-Type automatically
**Current Problem:**
We might be setting headers that conflict with multipart boundaries.
**Solution:**
```typescript
// DON'T set Content-Type manually for multipart uploads
const uploadResponse = await fetch(
`${tandoorConfig.serverUrl}/api/recipe/${recipeId}/image/`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`
// NO Content-Type header - let FormData set it
},
body: formData
}
);
```
**Key Point:** FormData automatically sets `Content-Type: multipart/form-data; boundary=...` and we must not override it.
**Acceptance Criteria:**
- No manual Content-Type header for image upload
- FormData handles multipart boundaries automatically
- Upload succeeds with proper headers
---
### Story 5: Add Comprehensive Error Logging
**Objective:** Better debugging for future issues
**Changes:**
```typescript
// Log request details
console.log(`[Tandoor Upload] Recipe ID: ${recipeId}`);
console.log(`[Tandoor Upload] Image source: ${imageUrl.substring(0, 100)}`);
console.log(`[Tandoor Upload] MIME type: ${mimeType}`);
console.log(`[Tandoor Upload] File size: ${buffer.length} bytes`);
// Log response details
if (!uploadResponse.ok) {
const responseText = await uploadResponse.text();
console.error(`[Tandoor Upload] Failed: ${uploadResponse.status}`);
console.error(`[Tandoor Upload] Response headers:`, uploadResponse.headers);
console.error(`[Tandoor Upload] Response body:`, responseText);
}
```
**Acceptance Criteria:**
- Response headers logged on error
- File metadata logged (size, type)
- Clear distinction between different error types
---
## Testing Strategy
### Test Case 1: Direct URL Image
```typescript
const imageUrl = 'https://www.giallozafferano.it/images/recipe_images/1087263_calamari-e-patate.jpg';
const result = await uploadRecipeImage(1, imageUrl);
// Expected: success: true
// Expected logs: File size, MIME type, success message
```
### Test Case 2: Base64 Screenshot
```typescript
const base64Url = 'data:image/jpeg;base64,/9j/4AAQSkZJRg...';
const result = await uploadRecipeImage(1, base64Url);
// Expected: success: true
// Expected logs: Detected base64, converted to file, success
```
### Test Case 3: Error Handling
```typescript
const invalidUrl = 'https://invalid.url/image.jpg';
const result = await uploadRecipeImage(1, invalidUrl);
// Expected: success: false, error message with details
```
---
## Code Example
### Complete Fixed Implementation
```typescript
export async function uploadRecipeImage(
recipeId: number,
imageUrl: string
): Promise<{ success: boolean; error?: string }> {
try {
const token = tandoorConfig.token;
if (!token) {
return { success: false, error: 'TANDOOR_TOKEN not set' };
}
console.log(`[Tandoor Upload] Recipe ID: ${recipeId}`);
console.log(`[Tandoor Upload] Image source: ${imageUrl.substring(0, 100)}...`);
let buffer: Buffer;
let mimeType: string;
let extension: string;
// Handle base64 data URL
if (isDataUrl(imageUrl)) {
console.log('[Tandoor Upload] Processing base64 data URL');
const parsed = parseDataUrl(imageUrl);
if (!parsed) {
return { success: false, error: 'Invalid data URL format' };
}
buffer = Buffer.from(parsed.base64Data, 'base64');
mimeType = parsed.mimeType;
extension = getExtensionFromMimeType(mimeType);
}
// Handle direct URL
else if (isDirectUrl(imageUrl)) {
console.log('[Tandoor Upload] Downloading from URL');
const response = await fetch(imageUrl);
if (!response.ok) {
return { success: false, error: `Failed to fetch image: ${response.statusText}` };
}
const arrayBuffer = await response.arrayBuffer();
buffer = Buffer.from(arrayBuffer);
mimeType = response.headers.get('content-type') || 'image/jpeg';
extension = getExtensionFromMimeType(mimeType);
}
else {
return { success: false, error: 'Invalid image URL format' };
}
console.log(`[Tandoor Upload] MIME type: ${mimeType}`);
console.log(`[Tandoor Upload] File size: ${buffer.length} bytes`);
console.log(`[Tandoor Upload] Extension: ${extension}`);
// Create proper File object for multipart upload
const blob = new Blob([buffer], { type: mimeType });
const file = new File([blob], `recipe-image${extension}`, { type: mimeType });
const formData = new FormData();
formData.append('image', file);
// Upload to Tandoor
const uploadResponse = await fetch(
`${tandoorConfig.serverUrl}/api/recipe/${recipeId}/image/`,
{
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`
// No Content-Type - let FormData set it
},
body: formData
}
);
if (!uploadResponse.ok) {
const errorText = await uploadResponse.text();
console.error(`[Tandoor Upload] Failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
console.error(`[Tandoor Upload] Response:`, errorText.substring(0, 500));
return {
success: false,
error: `Upload failed (${uploadResponse.status}): ${errorText.substring(0, 200)}`
};
}
console.log(`[Tandoor Upload] ✓ Success - ${buffer.length} bytes uploaded`);
return { success: true };
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
console.error(`[Tandoor Upload] Exception:`, error);
return { success: false, error: errorMsg };
}
}
```
---
## Key Differences from Previous Implementation
| Aspect | Previous | New |
|--------|----------|-----|
| URL Handling | URL pass-through first, then fallback | Always download and upload |
| Blob Creation | `new Blob()` only | `new Blob()` + `new File()` |
| MIME Type Source | Extension guessing | Actual HTTP headers or data URL |
| Error Handling | Multiple strategies with fallbacks | Single reliable path |
| Headers | May set Content-Type | Never set Content-Type for multipart |
---
## Success Metrics
**Primary Goal:**
- Images upload successfully to Tandoor without 400 or 500 errors
**Code Quality:**
- Single, reliable upload path
- Proper File object creation
- Clear error messages with response details
**Performance:**
- Minimal overhead from download
- No unnecessary retry attempts
- Fast failure with clear errors
---
## Rollback Plan
If the fix doesn't work:
1. Add detailed logging at each step
2. Test with curl to verify multipart format:
```bash
curl -X PUT \
-H "Authorization: Bearer ${TOKEN}" \
-F "image=@test-image.jpg" \
${TANDOOR_URL}/api/recipe/1/image/
```
3. Compare working curl request with our FormData
4. Investigate if SvelteKit/Node.js FormData implementation differs from browser
---
## Dependencies
- Node.js `Buffer` API
- Fetch API (built-in)
- FormData API (built-in)
- Blob/File constructors (built-in)
---
## References
- Tandoor OpenAPI Spec: `docs/Tandoor (2.3.6).yaml`
- Endpoint: `PUT /api/recipe/{id}/image/`
- Schema: `RecipeImage` (lines 13992-14005)
- Endpoint definition (lines 5712-5738)
---
**Plan Status:** ✅ Ready for Implementation
**Next Step:** Use `@dev FixTandoorImageUploadV2` to execute this plan

View File

@@ -5,7 +5,7 @@
"value": "SDRORLyWEsWWty2ZoVGdER", "value": "SDRORLyWEsWWty2ZoVGdER",
"domain": ".instagram.com", "domain": ".instagram.com",
"path": "/", "path": "/",
"expires": 1800849269.656302, "expires": 1800850168.116943,
"httpOnly": false, "httpOnly": false,
"secure": true, "secure": true,
"sameSite": "Lax" "sameSite": "Lax"
@@ -45,7 +45,7 @@
"value": "59661903731", "value": "59661903731",
"domain": ".instagram.com", "domain": ".instagram.com",
"path": "/", "path": "/",
"expires": 1774065269.656394, "expires": 1774066168.117026,
"httpOnly": false, "httpOnly": false,
"secure": true, "secure": true,
"sameSite": "None" "sameSite": "None"
@@ -72,7 +72,7 @@
}, },
{ {
"name": "rur", "name": "rur",
"value": "\"CLN\\05459661903731\\0541797825269:01fe6904bc7d85ccbfea5233062783089ac963caf6202742eb0b112bd5ab4f6ef965e2f4\"", "value": "\"CLN\\05459661903731\\0541797826168:01fe0c70391c9f4322366f6ea648ca6647818cd2ca16c215419a55558746287e2865245d\"",
"domain": ".instagram.com", "domain": ".instagram.com",
"path": "/", "path": "/",
"expires": -1, "expires": -1,
@@ -87,7 +87,7 @@
"localStorage": [ "localStorage": [
{ {
"name": "chatd-deviceid", "name": "chatd-deviceid",
"value": "11f6cbef-22a3-4c0b-9558-7a83fd40e521" "value": "0265e9b3-0083-498a-9424-7a08289dfb45"
}, },
{ {
"name": "hb_timestamp", "name": "hb_timestamp",
@@ -95,7 +95,7 @@
}, },
{ {
"name": "IGSession", "name": "IGSession",
"value": "6m2tlb:1766291070793" "value": "6m2tlb:1766291968512"
}, },
{ {
"name": "pixel_fire_ts", "name": "pixel_fire_ts",
@@ -107,7 +107,7 @@
}, },
{ {
"name": "Session", "name": "Session",
"value": "0f4qnx:1766289305793" "value": "nrg3hr:1766290203512"
}, },
{ {
"name": "has_interop_upgraded", "name": "has_interop_upgraded",

View File

@@ -372,15 +372,17 @@ function getExtensionFromMimeType(mimeType: string): string {
} }
/** /**
* Uploads an image to a Tandoor recipe with intelligent format handling * Uploads an image to a Tandoor recipe using proper multipart/form-data format
* *
* Supports three upload strategies: * Always downloads the image and uploads as a File object (not Blob).
* 1. Direct URL pass-through (most efficient) - for meta tags, Instagram URLs * This ensures proper multipart encoding with filename and MIME type metadata.
* 2. Base64 data URL conversion to file upload - for screenshots *
* 3. Fallback blob upload - for any other format * Handles two source formats:
* - Direct HTTP(S) URLs: Downloads from URL, detects MIME from response headers
* - Base64 data URLs: Decodes base64, uses embedded MIME type
* *
* @param recipeId - Tandoor recipe ID * @param recipeId - Tandoor recipe ID
* @param imageUrl - Image URL (can be HTTP(S) URL or base64 data URL) * @param imageUrl - Image URL (HTTP(S) URL or base64 data URL)
* @returns Success status and optional error message * @returns Success status and optional error message
*/ */
export async function uploadRecipeImage( export async function uploadRecipeImage(
@@ -397,111 +399,102 @@ export async function uploadRecipeImage(
console.log(`[Tandoor Upload] Image type: ${isDirectUrl(imageUrl) ? 'URL' : isDataUrl(imageUrl) ? 'Base64' : 'Unknown'}`); console.log(`[Tandoor Upload] Image type: ${isDirectUrl(imageUrl) ? 'URL' : isDataUrl(imageUrl) ? 'Base64' : 'Unknown'}`);
console.log(`[Tandoor Upload] Image source: ${imageUrl.substring(0, 100)}...`); console.log(`[Tandoor Upload] Image source: ${imageUrl.substring(0, 100)}...`);
// Strategy 1: Direct URL pass-through (preferred) let buffer: Buffer;
if (isDirectUrl(imageUrl)) { let mimeType: string;
console.log('[Tandoor Upload] Using URL pass-through strategy'); let sourceType: string;
const formData = new FormData();
formData.append('image_url', imageUrl);
const uploadResponse = await fetch( // Handle base64 data URLs
`${tandoorConfig.serverUrl}/api/recipe/${recipeId}/image/`,
{
method: 'PUT',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
}
);
if (uploadResponse.ok) {
console.log('[Tandoor Upload] ✓ Success via URL pass-through');
return { success: true };
}
// If URL strategy fails, fall through to file upload
const errorText = await uploadResponse.text().catch(() => uploadResponse.statusText);
console.warn(`[Tandoor Upload] URL pass-through failed (${uploadResponse.status}), trying file upload: ${errorText}`);
}
// Strategy 2: Base64 data URL to file upload
if (isDataUrl(imageUrl)) { if (isDataUrl(imageUrl)) {
console.log('[Tandoor Upload] Using base64 file upload strategy'); sourceType = 'base64';
console.log('[Tandoor Upload] Decoding base64 data URL');
const parsed = parseDataUrl(imageUrl); const parsed = parseDataUrl(imageUrl);
if (!parsed) { if (!parsed) {
return { success: false, error: 'Invalid data URL format' }; return { success: false, error: 'Invalid data URL format' };
} }
// Convert base64 to buffer buffer = Buffer.from(parsed.base64Data, 'base64');
const imageBuffer = Buffer.from(parsed.base64Data, 'base64'); mimeType = parsed.mimeType;
const extension = getExtensionFromMimeType(parsed.mimeType); console.log(`[Tandoor Upload] Decoded ${buffer.length} bytes, MIME: ${mimeType}`);
}
// Handle direct HTTP(S) URLs
else if (isDirectUrl(imageUrl)) {
sourceType = 'url';
console.log('[Tandoor Upload] Downloading image from URL');
// Create a proper file blob const response = await fetch(imageUrl);
const blob = new Blob([imageBuffer], { type: parsed.mimeType }); if (!response.ok) {
const formData = new FormData();
formData.append('image', blob, `recipe-image${extension}`);
const uploadResponse = await fetch(
`${tandoorConfig.serverUrl}/api/recipe/${recipeId}/image/`,
{
method: 'PUT',
headers: { 'Authorization': `Bearer ${token}` },
body: formData
}
);
if (!uploadResponse.ok) {
const errorText = await uploadResponse.text().catch(() => uploadResponse.statusText);
console.error(`[Tandoor Upload] Failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
console.error(`[Tandoor Upload] Response: ${errorText.substring(0, 200)}`);
return { return {
success: false, success: false,
error: `Upload failed (${uploadResponse.status}): ${errorText.substring(0, 200)}` error: `Failed to download image: ${response.status} ${response.statusText}`
}; };
} }
console.log(`[Tandoor Upload] ✓ Success via base64 file upload (${imageBuffer.length} bytes)`); // Get MIME type from response headers (most reliable)
return { success: true }; mimeType = response.headers.get('content-type') || 'image/jpeg';
// Remove charset if present (e.g., "image/jpeg; charset=utf-8")
mimeType = mimeType.split(';')[0].trim();
const arrayBuffer = await response.arrayBuffer();
buffer = Buffer.from(arrayBuffer);
console.log(`[Tandoor Upload] Downloaded ${buffer.length} bytes, MIME: ${mimeType}`);
}
// Unknown format
else {
return { success: false, error: 'Unsupported image format (not HTTP(S) URL or data URL)' };
} }
// Strategy 3: Fallback - try to fetch and upload // Create proper File object (critical for multipart/form-data)
console.log('[Tandoor Upload] Using fallback fetch strategy'); const extension = getExtensionFromMimeType(mimeType);
const response = await fetch(imageUrl); const filename = `recipe-image${extension}`;
const imageBlob = await response.blob();
// Determine file extension from blob type or default to jpg // In Node.js, we must create a File from Blob (Blob alone doesn't work)
let extension = '.jpg'; const blob = new Blob([buffer], { type: mimeType });
if (imageBlob.type) { const file = new File([blob], filename, { type: mimeType });
extension = getExtensionFromMimeType(imageBlob.type);
}
console.log(`[Tandoor Upload] Created File: ${filename} (${file.size} bytes, ${file.type})`);
// Upload to Tandoor
const formData = new FormData(); const formData = new FormData();
formData.append('image', imageBlob, `recipe-image${extension}`); formData.append('image', file);
console.log('[Tandoor Upload] Uploading to Tandoor...');
const uploadResponse = await fetch( const uploadResponse = await fetch(
`${tandoorConfig.serverUrl}/api/recipe/${recipeId}/image/`, `${tandoorConfig.serverUrl}/api/recipe/${recipeId}/image/`,
{ {
method: 'PUT', method: 'PUT',
headers: { 'Authorization': `Bearer ${token}` }, headers: {
'Authorization': `Bearer ${token}`
// DO NOT set Content-Type - let fetch set it with boundary
},
body: formData body: formData
} }
); );
if (!uploadResponse.ok) { if (!uploadResponse.ok) {
const errorText = await uploadResponse.text().catch(() => uploadResponse.statusText); const errorText = await uploadResponse.text().catch(() => uploadResponse.statusText);
const responseHeaders = JSON.stringify(Object.fromEntries(uploadResponse.headers.entries()));
console.error(`[Tandoor Upload] Failed: ${uploadResponse.status} ${uploadResponse.statusText}`); console.error(`[Tandoor Upload] Failed: ${uploadResponse.status} ${uploadResponse.statusText}`);
console.error(`[Tandoor Upload] Response: ${errorText.substring(0, 200)}`); console.error(`[Tandoor Upload] Response headers: ${responseHeaders}`);
console.error(`[Tandoor Upload] Response body: ${errorText.substring(0, 500)}`);
console.error(`[Tandoor Upload] File metadata: ${filename}, ${file.size} bytes, ${file.type}`);
return { return {
success: false, success: false,
error: `Upload failed (${uploadResponse.status}): ${errorText.substring(0, 200)}` error: `Upload failed (${uploadResponse.status}): ${errorText.substring(0, 200)}`
}; };
} }
console.log(`[Tandoor Upload] ✓ Success via fallback (${imageBlob.size} bytes)`); console.log(`[Tandoor Upload] ✓ Success (${sourceType}, ${file.size} bytes)`);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error'; const errorMsg = error instanceof Error ? error.message : 'Unknown error';
const errorStack = error instanceof Error ? error.stack : '';
console.error(`[Tandoor Upload] Exception: ${errorMsg}`); console.error(`[Tandoor Upload] Exception: ${errorMsg}`);
if (errorStack) {
console.error(`[Tandoor Upload] Stack: ${errorStack}`);
}
// Don't fail recipe creation if image fails // Don't fail recipe creation if image fails
return { success: false, error: errorMsg }; return { success: false, error: errorMsg };
} }