Skip to content

Commit 1fe0a0f

Browse files
committed
Add image proxy endpoint to fix broken image links
- Added /api/images/<conversation_id>/<filename> endpoint to proxy images from blob storage - Backend now saves proxy URLs instead of direct blob URLs - Frontend converts old blob URLs to proxy URLs when loading conversations - Fixes 403 Forbidden errors when accessing generated images
1 parent 03f310d commit 1fe0a0f

2 files changed

Lines changed: 60 additions & 4 deletions

File tree

content-gen/src/app.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,11 +315,17 @@ async def generate():
315315
try:
316316
blob_service = await get_blob_service()
317317
if response.get("image_base64"):
318-
image_url = await blob_service.save_generated_image(
318+
blob_url = await blob_service.save_generated_image(
319319
conversation_id=conversation_id,
320320
image_base64=response["image_base64"]
321321
)
322-
response["image_url"] = image_url
322+
# Convert blob URL to proxy URL for frontend access
323+
# blob_url format: https://account.blob.core.windows.net/container/conv_id/filename.png
324+
# proxy URL format: /api/images/conv_id/filename.png
325+
if blob_url:
326+
parts = blob_url.split("/")
327+
filename = parts[-1] # e.g., "20251202222126.png"
328+
response["image_url"] = f"/api/images/{conversation_id}/{filename}"
323329
except Exception as e:
324330
logger.warning(f"Failed to save image to blob storage: {e}")
325331

@@ -342,6 +348,8 @@ async def generate():
342348
)
343349

344350
# Save the full generated content for restoration
351+
# Note: image_base64 is NOT saved to CosmosDB as it exceeds document size limits
352+
# Images will only persist if blob storage is working
345353
generated_content_to_save = {
346354
"text_content": response.get("text_content"),
347355
"image_url": response.get("image_url"),
@@ -376,6 +384,40 @@ async def generate():
376384
)
377385

378386

387+
# ==================== Image Proxy Endpoint ====================
388+
389+
@app.route("/api/images/<conversation_id>/<filename>", methods=["GET"])
390+
async def proxy_generated_image(conversation_id: str, filename: str):
391+
"""
392+
Proxy generated images from blob storage.
393+
This allows the frontend to access images without exposing blob storage credentials.
394+
"""
395+
try:
396+
blob_service = await get_blob_service()
397+
await blob_service.initialize()
398+
399+
blob_name = f"{conversation_id}/{filename}"
400+
blob_client = blob_service._generated_images_container.get_blob_client(blob_name)
401+
402+
# Download the blob
403+
download = await blob_client.download_blob()
404+
image_data = await download.readall()
405+
406+
# Determine content type from filename
407+
content_type = "image/png" if filename.endswith(".png") else "image/jpeg"
408+
409+
return Response(
410+
image_data,
411+
mimetype=content_type,
412+
headers={
413+
"Cache-Control": "public, max-age=86400", # Cache for 24 hours
414+
}
415+
)
416+
except Exception as e:
417+
logger.exception(f"Error proxying image: {e}")
418+
return jsonify({"error": "Image not found"}), 404
419+
420+
379421
# ==================== Product Endpoints ====================
380422

381423
@app.route("/api/products", methods=["GET"])

content-gen/src/frontend/src/App.tsx

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,29 @@ function App() {
6666
}
6767
}
6868

69+
// Build image URL: convert old blob URLs to proxy URLs, or use existing proxy URL
70+
let imageUrl: string | undefined = gc.image_url;
71+
if (imageUrl && imageUrl.includes('blob.core.windows.net')) {
72+
// Convert old blob URL to proxy URL
73+
// blob URL format: https://account.blob.core.windows.net/container/conv_id/filename.png
74+
const parts = imageUrl.split('/');
75+
const filename = parts[parts.length - 1];
76+
const convId = parts[parts.length - 2];
77+
imageUrl = `/api/images/${convId}/${filename}`;
78+
}
79+
if (!imageUrl && gc.image_base64) {
80+
imageUrl = `data:image/png;base64,${gc.image_base64}`;
81+
}
82+
6983
const restoredContent: GeneratedContent = {
7084
text_content: typeof textContent === 'object' && textContent ? {
7185
headline: textContent?.headline,
7286
body: textContent?.body,
7387
cta_text: textContent?.cta,
7488
tagline: textContent?.tagline,
7589
} : undefined,
76-
image_content: (gc.image_url || gc.image_prompt) ? {
77-
image_url: gc.image_url,
90+
image_content: (imageUrl || gc.image_prompt) ? {
91+
image_url: imageUrl,
7892
prompt_used: gc.image_prompt,
7993
alt_text: gc.image_revised_prompt || 'Generated marketing image',
8094
} : undefined,

0 commit comments

Comments
 (0)