Fix 401 on image serving with presigned S3 URLs #17
@@ -123,22 +123,13 @@ async def upload_image():
|
|||||||
|
|
||||||
await s3_upload_image(processed_bytes, key, output_content_type)
|
await s3_upload_image(processed_bytes, key, output_content_type)
|
||||||
|
|
||||||
return jsonify(
|
return jsonify({"image_key": key})
|
||||||
{
|
|
||||||
"image_key": key,
|
|
||||||
"image_url": f"/api/conversation/image/{key}",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@conversation_blueprint.get("/image/<path:image_key>")
|
@conversation_blueprint.get("/image/<path:image_key>")
|
||||||
@jwt_refresh_token_required
|
@jwt_refresh_token_required
|
||||||
async def serve_image(image_key: str):
|
async def serve_image(image_key: str):
|
||||||
try:
|
url = await s3_presigned_url(image_key)
|
||||||
url = await s3_presigned_url(image_key)
|
|
||||||
except Exception:
|
|
||||||
return jsonify({"error": "Image not found"}), 404
|
|
||||||
|
|
||||||
return jsonify({"url": url})
|
return jsonify({"url": url})
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class ConversationService {
|
|||||||
async uploadImage(
|
async uploadImage(
|
||||||
file: File,
|
file: File,
|
||||||
conversationId: string,
|
conversationId: string,
|
||||||
): Promise<{ image_key: string; image_url: string }> {
|
): Promise<{ image_key: string }> {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
formData.append("conversation_id", conversationId);
|
formData.append("conversation_id", conversationId);
|
||||||
|
|||||||
@@ -9,10 +9,17 @@ type QuestionBubbleProps = {
|
|||||||
|
|
||||||
export const QuestionBubble = ({ text, image_key }: QuestionBubbleProps) => {
|
export const QuestionBubble = ({ text, image_key }: QuestionBubbleProps) => {
|
||||||
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
const [imageUrl, setImageUrl] = useState<string | null>(null);
|
||||||
|
const [imageError, setImageError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!image_key) return;
|
if (!image_key) return;
|
||||||
conversationService.getPresignedImageUrl(image_key).then(setImageUrl).catch(() => {});
|
conversationService
|
||||||
|
.getPresignedImageUrl(image_key)
|
||||||
|
.then(setImageUrl)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to load image:", err);
|
||||||
|
setImageError(true);
|
||||||
|
});
|
||||||
}, [image_key]);
|
}, [image_key]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -25,6 +32,12 @@ export const QuestionBubble = ({ text, image_key }: QuestionBubbleProps) => {
|
|||||||
"shadow-sm shadow-leaf/10",
|
"shadow-sm shadow-leaf/10",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{imageError && (
|
||||||
|
<div className="flex items-center gap-2 text-xs text-charcoal/50 bg-charcoal/5 rounded-xl px-3 py-2 mb-2">
|
||||||
|
<span>🖼️</span>
|
||||||
|
<span>Image failed to load</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{imageUrl && (
|
{imageUrl && (
|
||||||
<img
|
<img
|
||||||
src={imageUrl}
|
src={imageUrl}
|
||||||
|
|||||||
Reference in New Issue
Block a user