0

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("<", "&lt;")
                                modalBody.scrollTo({
                                    top: (modalBody.scrollHeight - modalBody.clientHeight),
                                    left: 0,
                                    behavior: 'instant'
                                })
                            } else {
                                rholder.innerHTML = ccontent + "<br>\n" + this_response.replaceAll("<", "&lt;")
                            }
                        }
                    }
                })
            }
        })
    }

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("<", "&lt;")
                    }
                }
            })
        }

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 ?

  • You should narrow this problem down more. Also, I wouldn't rely on browser buffering behavior with AJAX. If you need streaming, use the Fetch API. – Brad Jul 18 '23 at 22:44
  • Would you give me pointers as to which part you would like to see narrowed down? I'd be happy to help. And still it bothers me that for example '/watcher' endpoint works on same browser whilst '/instance-action' doesn't. '/watcher' endpoint contains a lot more data (as it's literally streaming whole log file' where the '/instance-action' only sends few lines. Also, i tried to send streaming response with headers: 'Cache-Control: no-store' to disable caching - still didn't work. – Marko Todoric Jul 19 '23 at 00:28
  • Related answers that you may also find helpful, in addition to the one above, can be found [here](https://stackoverflow.com/a/75837557/17865804) and [here](https://stackoverflow.com/a/76122475/17865804) – Chris Jul 19 '23 at 04:35

0 Answers0