I'm having issues handling StreamingResponse from FastAPI, specifically, i need to append every response from StreamingResponse into a modal (or any other HTML element) body. However, for whatever reason - result is inconsitent.
I have an endpoint /watcher
and once action that serves StreamingResponse from FastAPI endpoint is triggered by javascript below, i get the expected result and modal body is populated each time response is sent. And it's handled on Chrome (Windows 114.0.5735.199), Firefox (Windows 114.0.2) and Firefox (Linux Fedora 107.0) just fine.
function tailLog(servername) {
try {
document.getElementsByClassName("modal-dialog modal-dialog-centered modal-xl")[0].classList = "modal-dialog modal-dialog-centered modal-fullscreen"
} catch {
// none
}
var last_response_len = false;
tg = document.getElementById("wfDebugger-body")
tg.innerHTML = "<p>Info msg.</p>"
btnholder = document.getElementById("wfDebugger-btn-holder")
btnholder.innerHTML = ""
modalHeader = document.getElementById("wfDebugger-header")
modalHeader.innerHTML = ""
modalHeaderTitle = document.createElement("h5")
modalHeaderTitle.setAttribute("class", "modal-title")
modalHeaderTitle.innerHTML = "Live Log Viewer"
modalHeader.appendChild(modalHeaderTitle)
rholder = document.createElement("code")
rholder.id = "responseHolder"
rholder.style.whiteSpace = "pre"
tg.appendChild(rholder)
btn = document.createElement("button")
btn.setAttribute("type", "button")
btn.setAttribute("class", "btn btn-secondary ms-3")
btn.setAttribute("onclick", "stopTailLog(this)")
btn.setAttribute("data-servername", servername)
btn.innerHTML = "Stop View"
closebtn = document.createElement("button")
closebtn.setAttribute("id", "wfDebugger-close-btn")
closebtn.setAttribute("type", "button")
closebtn.setAttribute("class", "btn btn-info ms-3")
closebtn.setAttribute("data-bs-dismiss", "modal")
closebtn.setAttribute("data-bs-target", "wfDebugger")
closebtn.setAttribute("onclick", "stopTailLog(this)")
closebtn.setAttribute("data-servername", servername)
closebtn.innerHTML = "Close"
modalCloseDiv = document.createElement("div")
modalCloseDiv.setAttribute("class", "modal-title")
modalCloseDiv.appendChild(btn)
modalCloseDiv.appendChild(closebtn)
modalHeader.appendChild(modalCloseDiv)
$.ajax({
url: "/watcher/" + servername + "?action=wildfly_getlogid",
success: function(data) {
btn.setAttribute("data-logid", data)
closebtn.setAttribute("data-logid", data)
$.ajax({
url: "/watcher/" + servername + "/wildfly_getlog?data=" + data,
method: "POST",
complete: function() {
$.ajax({ url: "/watcher/" + servername + "/wildfly_killlog?data=" + data, method: "POST" })
},
xhrFields: {
onprogress: function(e) {
var this_response, response = e.currentTarget.response;
if(last_response_len === false)
{
this_response = response;
last_response_len = response.length;
}
else
{
this_response = response.substring(last_response_len);
last_response_len = response.length;
}
ccontent = rholder.innerHTML
modalBody = document.getElementById("wfDebugger-body")
if (modalBody.scrollTop == (modalBody.scrollHeight - modalBody.clientHeight)) {
rholder.innerHTML = ccontent + "<br>\n" + this_response.replaceAll("<", "<")
modalBody.scrollTo({
top: (modalBody.scrollHeight - modalBody.clientHeight),
left: 0,
behavior: 'instant'
})
} else {
rholder.innerHTML = ccontent + "<br>\n" + this_response.replaceAll("<", "<")
}
}
}
})
}
})
}
However, when i try the same on another page that calls another endpoint, with similar javascript code, i only get the response in full on Chrome (Windows 114.0.5735.199) once it's completed, but works fine on both Firefox in Windows and Linux. I have no idea why. This is the code that is causing issues on Chrome.
function startStopInstance(action, instanceId, instanceName, btn, refreshBtn) {
modal = initModal()
var last_response_len = false;
rholder = document.createElement("code")
rholder.id = "responseHolder"
rholder.style.whiteSpace = "pre"
DevOpsModalBody.appendChild(rholder)
var title
if (action == 'start') {
title = `Starting instance: '${instanceName}'...`
}
else if (action == 'stop') {
title = `Stopping instance: '${instanceName}'...`
}
else if (action == 'restart') {
title = `Restarting instance: '${instanceName}'...`
}
else if (action == "increase_volume_by_") {
title = `Increasing volume size for instance: '${instanceName}'...`
const _size = document.getElementById(`size_${instanceId}`).value
action = `increase_volume_by_${_size}`
}
const spinner = createSpinner()
const modalSpinner = createSpinner()
// Disable btn while request is running
btn.disabled = true
btn.appendChild(spinner)
DevOpsModalTitle.textContent = title
DevOpsModalTitle.appendChild(modalSpinner)
DevOpsModalCloseBtn.hidden = true
DevOpsModalSaveBtn.hidden = true
modal.show()
$.ajax({
url: `/instance-action/${instanceId}/${action}`,
method: "POST",
complete: function() {
btn.disabled = false;
spinner.remove();
modalSpinner.remove();
DevOpsModalCloseBtn.hidden = false;
},
xhrFields: {
onprogress: function(e) {
console.log(e)
var this_response, response = e.currentTarget.response;
if(last_response_len === false)
{
this_response = response;
last_response_len = response.length;
}
else
{
this_response = response.substring(last_response_len);
last_response_len = response.length;
}
ccontent = rholder.innerHTML
rholder.innerHTML = ccontent + "<br>\n" + this_response.replaceAll("<", "<")
}
}
})
}
Above you'll see createElement
and createSpinner
but those functions are nothing more but a shortcut of couple of document.createElement
with specific attributes to get the element i need.
xhrFields callback is changed to not check if user is already viewing last line in order to scroll, instead just appends the data - so quite simpler but still not working on Chrome.
Both endpoints have similar return statement in FastAPI:
return StreamingResponse(getwflogs(serverip, data), media_type="text/plain") # The one that works on all three browsers
return StreamingResponse(func(instance_id), media_type='text/plain') # The one that works only in firefox
async def getwflogs(serverip, logid):
async with AsyncClient(headers=api_auth, timeout=None) as client:
req = client.build_request("POST", f"http://{serverip}:3322/api/service/getlog/{logid}")
r = await client.send(req, stream=True)
if r.status_code == 200:
async for i in r.aiter_text():
yield i
async def func(instance_id: str, size: int):
cfg = load_settings()
session = aioboto3.Session(aws_access_key_id=cfg.aws.key, aws_secret_access_key=cfg.aws.secret_key, region_name=cfg.aws.region)
partition_number = 0
async with session.resource('ec2') as res, session.client('ec2') as client:
yield "Loading instance information\n"
instance = await res.Instance(instance_id)
async for v in instance.volumes.all():
partition_number += 1
yield f"Processing volume id: {v.id}. Increasing size from {await v.size} to {await v.size + size}...\n"
_res = await client.modify_volume(DryRun=False, VolumeId=v.id, Size=await v.size + size)
if jpsearch("VolumeModification.ModificationState", _res) == 'modifying':
_res = await client.describe_volumes_modifications(VolumeIds=[v.id])
while not jpsearch("VolumesModifications[].ModificationState", _res) == ['completed']:
_res = await client.describe_volumes_modifications(VolumeIds=[v.id])
yield f"Size of volume id: {v.id} successfully increased. Please go to server and execute:\nsudo growpart /dev/nvme0n1 {partition_number}\nsudo resize2fs /dev/nvme0n1p{partition_number}\n"
Any idea what could be causing this behaviour. Why Chrome gets streamingresponse in full after it's completed entirely for 1 of two pages/endpoints but firefox works fine every time ?