'How to send a base64 encoded image to a FastAPI backend
I'm using code from this and that answer to send a base64 encoded image to a python FastAPI backend. The client side looks like this:
function toDataURL(src, callback, outputFormat) {
var img = new Image();
img.crossOrigin = 'Anonymous';
img.onload = function() {
var canvas = document.createElement('CANVAS');
var ctx = canvas.getContext('2d');
var dataURL;
canvas.height = this.naturalHeight;
canvas.width = this.naturalWidth;
ctx.drawImage(this, 0, 0);
dataURL = canvas.toDataURL(outputFormat);
callback(dataURL);
};
img.src = src;
if (img.complete || img.complete === undefined) {
img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==";
img.src = src;
}
}
function makeBlob(dataURL) {
var BASE64_MARKER = ';base64,';
if (dataURL.indexOf(BASE64_MARKER) == -1) {
var parts = dataURL.split(',');
var contentType = parts[0].split(':')[1];
var raw = decodeURIComponent(parts[1]);
return new Blob([raw], { type: contentType });
}
var parts = dataURL.split(BASE64_MARKER);
var contentType = parts[0].split(':')[1];
var raw = window.atob(parts[1]);
var rawLength = raw.length;
var uInt8Array = new Uint8Array(rawLength);
for (var i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
}
...
toDataURL(
images[0], // images is an array of paths to images
function(dataUrl) {
console.log('RESULT:', dataUrl);
$.ajax({
url: "http://0.0.0.0:8000/check/",
type: 'POST',
processData: false,
contentType: 'application/octet-stream',
data: makeBlob(dataUrl)
}).done(function(data) {console.log("success");}).fail(function() {console.log("error");});
}
);
And the server side is as follows:
@app.post("/check")
async def check(file: bytes = File(...)) -> Any:
// do something here
I'm only showing the signature of the endpoint because for now nothing much is happening in it anyway.
Here is the output of the backend when I call it as shown above:
172.17.0.1:36464 - "OPTIONS /check/ HTTP/1.1" 200
172.17.0.1:36464 - "POST /check/ HTTP/1.1" 307
172.17.0.1:36464 - "OPTIONS /check HTTP/1.1" 200
172.17.0.1:36464 - "POST /check HTTP/1.1" 422
So in short I keep getting 422 error codes, which means that there is a mismatch between what I send and what the endpoint expects, but even after some reading I'm still not clear on what exactly is the issue. Any help would be most welcome!
Solution 1:[1]
As previously mentioned, uploaded files are sent as form
data. As per FastAPI documentation:
Data from forms is normally encoded using the "media type"
application/x-www-form-urlencoded
when it doesn't include files.But when the form includes files, it is encoded as
multipart/form-data
. If you useFile
, FastAPI will know it has to get the files from the correct part of the body.
Regardless of what type you used, either bytes
or UploadFile
, since...
If you declare the type of your path operation function parameter as
bytes
, FastAPI will read the file for you and you will receive the contents as bytes.
Hence, the 422 Unprocessable entity error. In your example, you send binary data (using application/octet-stream
for content-type
), however, your API's endpoint expects form
data (i.e., multipart/form-data
).
Option 1
Instead of sending a base64 encoded image, upload the file as it is, either using HTML Forms, as shown here, or Javascript, as shown below. As others pointed out, it is imperative that you set the contentType option to false, when using JQuery. With pure javascript, as below (credits for the method), similar issues as described in the link above occured when setting the content-type
manually; thus, best to leave it out and force the browser to set it (along with the mandatory multipart boundary).
server side:
@app.post("/upload")
async def upload(file: UploadFile = File(...)):
try:
contents = await file.read()
with open("uploaded_" + file.filename, "wb") as f:
f.write(contents)
except Exception:
return {"message": "There was an error uploading the file"}
finally:
await file.close()
return {"message": f"Successfuly uploaded {file.filename}"}
client side:
<script>
function uploadFile(){
var file = document.getElementById('fileInput').files[0];
if(file){
var xhr = new XMLHttpRequest();
var url = "/upload";
xhr.open("POST", url, true);
//xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
var formData = new FormData();
formData.append("file", file);
xhr.send(formData);
}
}
</script>
<input type="file" id="fileInput" name="file"><br>
<input type="button" value="Upload File" onclick="uploadFile()">
If you would like to use Axios
library for the upload, please have a look at this answer.
Option 2
If you still need to upload a base64 encoded image, you can send the data as form
data, using application/x-www-form-urlencoded
as the content-type
; while in your API's endpoint, you can define a Form
field to receive the data. Below is a complete working example, where a base64 encoded image is sent, received by the server, decoded and saved to the disk. For base64 encoding, the readAsDataURL method is used on client side. Please note, the file writing to the disk is done using synchronous writing. In scenarios where multiple (or large) files need to be saved, it would be best to use async
writing, as described here.
app.py
from fastapi import Form, Request, FastAPI
from fastapi.responses import HTMLResponse
from fastapi.templating import Jinja2Templates
import base64
app = FastAPI()
templates = Jinja2Templates(directory="templates")
@app.post("/upload")
async def upload(filename: str = Form(...), filedata: str = Form(...)):
image_as_bytes = str.encode(filedata) # convert string to bytes
img_recovered = base64.b64decode(image_as_bytes) # decode base64string
try:
with open("uploaded_" + filename, "wb") as f:
f.write(img_recovered)
except Exception:
return {"message": "There was an error uploading the file"}
return {"message": f"Successfuly uploaded {filename}"}
@app.get("/")
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
templates/index.html
<script>
function previewFile() {
const preview = document.querySelector('img');
const file = document.querySelector('input[type=file]').files[0];
const reader = new FileReader();
reader.addEventListener("load", function () {
preview.src = reader.result; //show image in <img tag>
base64String = reader.result.replace("data:", "").replace(/^.+,/, "");
//to prevent plus signs ('+') from being stripped out and replaced by spaces
var encodedbase64String = encodeURIComponent(base64String);
uploadFile(file.name, encodedbase64String)
}, false);
if (file) {
reader.readAsDataURL(file);
}
}
function uploadFile(filename, filedata){
var xhr = new XMLHttpRequest();
var url = "http://127.0.0.1:8000/upload";
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.onreadystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
formdata = "filename=" + filename + "&filedata=" + filedata
xhr.send(formdata);
}
</script>
<input type="file" onchange="previewFile()"><br>
<img src="" height="200" alt="Image preview...">
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 |