I am trying to display an image which is stored as binary data in my MongoDB database. I have this functionality working well for User images, but cannot get it to work for products. I'm using FastAPI, React, and MongoDB as my stack.
I have a list of products being stored in my MongoDB products collection, which has the following structure:
class Products(BaseModel):
    _id: str = Field(alias="_id")
    productName: str
    productID: str
    photographer: str
    productEvent: str
    productTags: Optional[List[str]] = None
    productPrice: float
    productCurrency: str
    productDate: Optional[datetime.date] = None
    productImage: Optional[bytes] = None
    productImageExtension: Optional[str] = None
The 'productImage' field is a binary field in the mongoDB collection. I can populate this with data using the following form perfectly:
import React, {useState, useEffect} from 'react';
import axios from 'axios';
function ProductUpload(){
  const [productName, setProductName] = useState('');
  const [productID, setProductID] = useState('');
  const [photographer, setPhotographer] = useState('');
  const [productEvent, setProductEvent] = useState('');
  const [productTags, setProductTags] = useState([]);
  const [productPrice, setProductPrice] = useState(0);
  const [productCurrency, setProductCurrency] = useState('');
  const [productDate, setProductDate] = useState('');
  const [productImage, setProductImage] = useState(null);
  const [error, setError] = useState('');
  const handleSubmit = async (event) => {
    event.preventDefault();
  
    const formData = new FormData();
    formData.append('productName', productName);
    formData.append('productID', productID);
    formData.append('photographer', photographer);
    formData.append('productEvent', productEvent);
    formData.append('productTags', JSON.stringify(productTags));
    formData.append('productPrice', productPrice);
    formData.append('productCurrency', productCurrency);
    formData.append('productDate', productDate);
    formData.append('productImage', productImage);
  
    try {
      const response = await axios.post('http://localhost:8000/api/products', formData);
      console.log(response.data);
    } catch (error) {
      console.log(error.response.data);
      setError(error.response.data.detail);
    }
  };
  return(
    <form onSubmit={handleSubmit} style={{alignContents: "center", justifyContent: "center", display: "flex", flexDirection: "column", margin: "auto", width: "100%"}}>
      <label>
        Product Name:
        <input type="text" value={productName} onChange={(event) => setProductName(event.target.value)} />
      </label>
      <label>
        Product ID:
        <input type="text" value={productID} onChange={(event) => setProductID(event.target.value)} />
      </label>
      <label>
        Photographer:
        <input type="text" value={photographer} onChange={(event) => setPhotographer(event.target.value)} />
      </label>
      <label>
        Product Event:
        <input type="text" value={productEvent} onChange={(event) => setProductEvent(event.target.value)} />
      </label>
      <label>
        Product Tags:
        <input type="text" value={productTags} onChange={(event) => setProductTags(event.target.value.split(','))} />
      </label>
      <label>
        Product Price:
        <input type="number" value={productPrice} onChange={(event) => setProductPrice(event.target.value)} />
      </label>
      <label>
        Product Currency:
        <input type="text" value={productCurrency} onChange={(event) => setProductCurrency(event.target.value)} />
      </label>
      <label>
        Product Date:
        <input type="date" value={productDate} onChange={(event) => setProductDate(event.target.value)} />
      </label>
      <label>
        Product Image:
        <input type="file" onChange={(event) => setProductImage(event.target.files[0])} />
      </label>
      <button type="submit">Submit product</button>
      </form>
  )
}
export default ProductUpload;
Here are my backend functions relating to Products:
main.py:
@app.get("/api/products")
async def get_products():
    response = await fetch_all_products()
    return response
@app.get("/api/products{productName}", response_model=Products)
async def get_product_by_name(productName):
    response = await fetch_one_product(productName)
    if response:
        return response
    raise HTTPException(404, f"There is no Product item with this title: {productName}")
@app.get("/api/products/image/{productID}")
async def get_product_img(productID: str):
    image_data, image_extension = await fetch_product_image(productID)
    if image_data:
        return StreamingResponse(io.BytesIO(image_data), media_type=f"image/{image_extension}")
    raise HTTPException(404, f"There is no Product with this ID: {productID}")
@app.post("/api/products")
async def post_product(
    productName: str = Form(...),
    productID: str = Form(...),
    photographer: str = Form(...),
    productEvent: str = Form(...),
    productTags: str = Form(...),
    productPrice: float = Form(...),
    productCurrency: str = Form(...),
    productDate: str = Form(...),
    productImage: UploadFile = File(...)
):
    productTagsList = [tag.strip() for tag in productTags.split(',')]
    productDateObj = datetime.datetime.strptime(productDate, "%Y-%m-%d").date()
    image_data = await productImage.read()
    img = Image.open(io.BytesIO(image_data))
    original_format = img.format
    
    productImageExtension = original_format.lower()
    binary_image_data = Binary(image_data)
    product = Products(
        productName=productName,
        productID=productID,
        photographer=photographer,
        productEvent=productEvent,
        productTags=productTagsList,
        productPrice=productPrice,
        productCurrency=productCurrency,
        productDate=productDateObj,
        productImage=binary_image_data,
        productImageExtension=productImageExtension
    )
    response = await create_product(product.dict())
    if response:
        return response
    raise HTTPException(400, "Something went wrong / Bad Request")
@app.put("/api/products{productName}/", response_model=Products)
async def put_product(productName:str, productID:str, photographer:str, productEvent:str, productTags:List[str], productPrice:float,
                    productCurrency: str, productDate: datetime.date, productImage: bytes):
        response = await update_product(productName, productID, photographer, productEvent, productTags,
                                      productPrice, productCurrency, productDate, productImage)
        if response:
            return response
        raise HTTPException(400, "Something went wrong / Bad Request")
@app.delete("/api/products{productName}")
async def delete_product(productName):
    response = await remove_product(productName)
    if response:
        return "Successfully deleted Product Item"
    raise HTTPException(400, "Something went wrong / Bad Request")
database.py
async def fetch_one_product(name):
    document = await database.products.find_one({"productName":name})
    return document
async def fetch_all_products():
    productsL = []
    cursor = database.products.find({})
    async for document in cursor:
        productsL.append(Products(**document))
    return productsL
async def create_product(product: dict):
    product['productDate'] = datetime.combine(product['productDate'], datetime.min.time())
    document = product
    result = await database.products.insert_one(document)
    document["_id"] = result.inserted_id
    product_instance = Products(**document)
    return product_instance.to_dict()
async def update_product(productName, productID, photographer, productEvent, productTags, productPrice, productCurrency, productDate, productImage):
    await database.products.update_one({"name":productName},{"$set":{"productName": productName, "productID": productID, "photographer": photographer,
                                                                   "productEvent": productEvent, "productTags": productTags, "productPrice": productPrice,
                                                                   "productCurrency": productCurrency, "productDate": productDate, "productImage": productImage}})
    document = await database.products.find_one({"name":productName})
    return document
async def remove_product(name):
    await database.products.delete_one({"name":name})
    return True
async def fetch_one_product_byID(productID):
    document = await database.products.find_one({"productID": productID})
    return document
async def fetch_product_image(productID: str):
    product = await database.products.find_one({"productID": productID}, {"productImage": 1, "productImageExtension": 1})
    if product:
        image_data = bytes(product["productImage"])
        image_extension = product["productImageExtension"]
        return image_data, image_extension
    return None, None
I am trying to display the productImage using a ProductImage React component, which is as follows:
import React, { useState, useEffect } from "react";
import axios from "axios";
const ProductImage = ({ productID, size = "normal" }) => {
  const [image, setImage] = useState(null);
  let imageSize = "250px";
  if(size === "small"){
    imageSize = "20px";
  }
  useEffect(() => {
    const fetchImage = async () => {
      try {
        const response = await axios.get(`http://localhost:8000/api/products/image/${productID}`, {
          responseType: "blob",
        });
        setImage(URL.createObjectURL(response.data));
      } catch (error) {
        console.error(error);
      }
    };
    fetchImage();
  }, [productID]);
  return <img src={image} alt={productID} style={{borderRadius: "50%", width: imageSize, height: imageSize}}/>;
};
export default ProductImage;
It seems to return errors relating to the image (I think), here's an error which appears when I try to access the page where the images should be rendering:
 File "pydantic\json.py", line 45, in pydantic.json.lambda
UnicodeDecodeError: 'utf-8' codec can't decode byte 0x89 in position 0: invalid start byte
I just can't seem to pinpoint where the issue is, but I'm fairly sure it is to do with the image and maybe how its being stored or processed. I know it's not the best practice to store images like this, but it's just a personal project to get an idea. I have the code working for displaying user images, here's a short sample of how that looks:
Backend:
@app.post("/api/User/image")
async def upload_user_img(email: str = Form(...), image: UploadFile = File(...)):
    res = await user_image_upload(email, image)
    return res
@app.get("/api/User/image/{email}")
async def get_user_img(email: str):
    image_data = await fetch_user_image(email)
    if image_data:
        return StreamingResponse(io.BytesIO(image_data), media_type="image/png")
    raise HTTPException(404, f"There is no User with this email: {email}")
async def user_image_upload(email: str, image: UploadFile = File(...)):
    user_email = email
    user = database.User.find_one({"email": user_email})
    if user:
        image_data = await image.read()
        binary_image_data = Binary(image_data)
        database.User.update_one({"email": user_email}, {"$set": {"image": binary_image_data}})
        return {"message": "Image uploaded successfully"}
    else:
        return {"message": "User not found"}
    
async def fetch_user_image(email):
    user = await database.User.find_one({"email": email}, {"image": 1})
    return user["image"]
Frontend:
import React, { useState, useEffect } from "react";
import axios from "axios";
const UserImage = ({ email, size = "normal" }) => {
  const [image, setImage] = useState(null);
  let imageSize = "250px";
  if(size === "small"){
    imageSize = "20px";
  }
  useEffect(() => {
    const fetchImage = async () => {
      try {
        const response = await axios.get(`http://localhost:8000/api/User/image/${email}`, {
          responseType: "blob",
        });
        setImage(URL.createObjectURL(response.data));
      } catch (error) {
        console.error(error);
      }
    };
    fetchImage();
  }, [email]);
  return <img src={image} alt={email} style={{borderRadius: "50%", width: imageSize, height: imageSize}}/>;
};
export default UserImage;
Data model for User:
class User(BaseModel):
    _id: str
    name: Optional[str] = None
    email: str
    phone: Optional[str] = None
    password: str
    active: Optional[bool] = None
    image: Optional[bytes] = None
    dob: Optional[datetime.datetime] = None
    weight: Optional[float] = None
    club: Optional[str] = None
    category: Optional[str] = None
    location: Optional[str] = None
    bank_details: Optional[bankDetails] = None
    isAdmin: Optional[bool] = None
    type: Optional[str] = None
It's worth noting that I call this UserImage react component from various places and it works well, and calling the ProductImage component is done in the same way, so I'm not sure what is going on.
Apologies for the long question, any advice helpful!
