1

I am trying to consume an API endpoint that returns a stream of text. When I open the endpoint directly in my browser address bar, it works fine and it is clear that it is in fact streaming the response and not just returning it all at once.

However, although it works, I can see in the console that I get a ERR_INCOMPLETE_CHUNKED_ENCODING error.

This error is a likely reason why my client app is unable to consume this endpoint.

However, when I call the endpoint from my Blazor app, I get this error/stacktrace which does not really tell me what the problem is.

Here is my code in my Blazor app that is calling the endpoint:

using var request = new HttpRequestMessage(HttpMethod.Get, path);
request.SetBrowserResponseStreamingEnabled(true);
                        
using var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
            
var buffer = new char[1024];
int bytesRead;

while ((bytesRead = await reader.ReadBlockAsync(buffer, 0, buffer.Length)) > 0)
{
    var text = new string(buffer, 0, bytesRead);
    Console.Write(text);
    yield return text;
}

The error happens on the while line above.

When I look in the Network tab, then the preflight (OPTIONS) call is successful and the actual fetch call is also 200 OK but the response seems empty in browsers Developer Tools.

This is what it looks like in the browser console.

enter image description here

I am already doing 100s of API calls from this Blazor app to that API so the difference here is that this particular endpoint returns a stream of text instead of just a normal json response.

  1. What could be the cause of the error?

and/or

  1. How can I find the actual error that is happening?

Someone mentioned the issue could be related to the serverside code. That could very well be right as the error is also shown when just inserting the endpoint in the address bar of the browser.

I am including the .NET 6 Azure Function here:

[FunctionName("RespondWithStream")]
public static async Task Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "stream-text")] HttpRequest req,
    ILogger log)
{
    try
    {
        List<string> list = new List<string>
        {
            "text1",
            "text2",
            "text3"
        };
                
        var response = req.HttpContext.Response;
        response.StatusCode = 200;
        response.ContentType = "application/json";
        response.Headers.Add("Access-Control-Allow-Origin", "*");
        response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
        response.Headers.Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, photish-tokenkey");
        response.Headers.Add("Access-Control-Allow-Credentials", "true");

        StringBuilder sb = new();
        await using var sw = new StreamWriter(response.Body);
        foreach (var item in list)
        {
            sb.Append(item);
            await sw.WriteAsync(item);
            await sw.FlushAsync();

            Thread.Sleep(2000);
        }
    }
    catch (Exception exc)
    {
        log.LogError(exc, "Issue when streaming: " + exc.Message);
    }
}

Solution

Here is the final code that solved the issue. I believe the key was CompleteAsync and that was unavailable before I switched to IHttpResponseBodyFeature

[FunctionName("RespondWithStream")]
public static async Task Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "stream-text")] HttpRequest req,
    ILogger log)
{
    var response = req.HttpContext.Response;
    var responseBody = response.HttpContext.Features.Get<IHttpResponseBodyFeature>();
    responseBody.DisableBuffering();
    response.StatusCode = 200;
    response.ContentLength = null;
    response.ContentType = "text/plain";

    await using var sw = new StreamWriter(response.Body);
    for (int i = 0; i < 100; i++)
    {
        await responseBody.Writer.WriteAsync(Encoding.UTF8.GetBytes(i+"."));
        await responseBody.Writer.FlushAsync();
        await Task.Delay(100);
    }

    await responseBody.CompleteAsync();
}
Niels Brinch
  • 3,033
  • 9
  • 48
  • 75
  • 1
    I think this question already has an answer, but implementation differs a little bit. Did you try to use this approach - https://stackoverflow.com/a/48565348/3887114 ? If I understood your question correctly, provided answer on this thread does exactly the same thing as you described. If you are trying to achieve something else, please explain - what & why? Alternatively, I could think of using WebClient as was mentioned here: https://stackoverflow.com/a/7543339/3887114 – Irakli Mar 16 '23 at 16:19
  • Thank you. First answer is about ReadLine and I am also not using ReadLine in my code for the same reason. However, I will try with the code from that answer just in case. Second answer is about "ReadToEnd" and this does not conform with my requirement as I do need to STREAM the response. – Niels Brinch Mar 17 '23 at 12:12

3 Answers3

2

What happens if you replace the code with something like the following:

using var request = new HttpRequestMessage(HttpMethod.Get, path);
request.SetBrowserResponseStreamingEnabled(true);
                        
using var response = await http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
using var stream = await response.Content.ReadAsStreamAsync();
using var reader = new StreamReader(stream);
            
string text = await reader.ReadToEndAsync();

Curious if the behavior changes or if it just works. If you know the size of the text being returned is not overly large, this may do.

Server side code:

[FunctionName("RespondWithStream")]
public static async Task Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "stream-text")] HttpRequest req,
    ILogger log)
{
    try
    {
        List<string> list = new List<string>
        {
            "text1",
            "text2",
            "text3"
        };
                
        var response = req.HttpContext.Response;
        var responseBody = response.HttpContext.Features.Get<IHttpResponseBodyFeature>();
        responseBody.DisableBuffering();
        response.StatusCode = 200;
        response.ContentLength = null;
        response.ContentType = "text/plain";

        await using var sw = new StreamWriter(response.Body);

        foreach (var item in list)
        {
           responseBody.Writer.WriteAsync(Encoding.UTF8.GetBytes( item ));
           await responseBody.Writer.FlushAsync();
        }

        await responseBody.CompleteAsync();

    }
    catch (Exception exc)
    {
        log.LogError(exc, "Issue when streaming: " + exc.Message);
    }
}
bdcoder
  • 3,280
  • 8
  • 35
  • 55
  • Thank you. This may help troubleshooting. However, I must mention that the objective is certainly to STREAM the response and not to wait for the entire stream to be completed. Here is the error I get when implementing your code above - I will use this to troubleshoot: net::ERR_INCOMPLETE_CHUNKED_ENCODING 200 (OK) – Niels Brinch Mar 17 '23 at 11:27
  • Ok, have you checked the code that is SENDING the response?? -- it may be that you have a Response.Flush() followed by a Response.Close() call -- if so, try replacing those two calls with a Response.End() instead. – bdcoder Mar 18 '23 at 01:20
  • Thanks, I have included my serverside code in the question. As far as I can see it is correct, but it is certainly possible the issue comes from here. I have no Response.End available as this is a HttpResponse. – Niels Brinch Mar 19 '23 at 11:28
  • I can confirm that I can reproduce the same issue with the simple serverside code provided above. – Niels Brinch Mar 19 '23 at 11:54
  • Not sure it it will help, but have you tried changing: response.ContentType="text/plain" and to minimize the response code? I have updated my original answer with minimum response code as well - If that works, then you can try to add back the streaming code. – bdcoder Mar 19 '23 at 16:11
  • With your exact code (without StreamWriter) it doesn't actually return any content any more, but still gets ERR_INCOMPLETE_CHUNKED_ENCODING. And when just changing to text/plain, the result is the same as before. Bonus info: Even though it looks like it works when calling directly from the address bar, I noticed even then it still shows ERR_INCOMPLETE_CHUNKED_ENCODING so that tells me the issue IS first and foremost serverside. – Niels Brinch Mar 19 '23 at 18:54
  • @NielsBrinch - I would agree - get the server side working first with a minimal amount of code (i.e.: send a single "Hello World" line of text back to the client). Once that is working add back one component at a time until it breaks ... or ... hopefully, works. What happens if you remove all headers? ( I updated my server side code to reflect). – bdcoder Mar 19 '23 at 19:42
  • Your sparring helped me find the issue. You can see the solution in the question. Perhaps you'd like to adjust the sample code in your answer to not confuse future people who find this question? – Niels Brinch Mar 19 '23 at 21:44
  • @NielsBrinch - Great job! -- I edited the sample code in my answer to mirror your solution as per your request. – bdcoder Mar 20 '23 at 00:36
2

Cause & Solution #1: HTTP/2

Minimal reproduction of the HTTP/2 problem: https://github.com/abberdeen/Streaming

Why does this error occur? Blazor either buffers the request bodies, or (when using HTTP/1.1) doesn't expect chunked data transfer encoding to be used, and somehow unbelievably stops working.

How to get around the problem with HTTP/2 (net::ERR_HTTP2_PROTOCOL_ERROR) while streaming requests?

Solution 1.1. Use HTTP instead of HTTPS on API:

Response.Headers["Content-Encoding"] = "identity";
Response.Headers["Transfer-Encoding"] = "identity";

Solution 1.2. Do not close/dispose the stream on the server, and try to transfer data without closing the stream by staying on HTTPS.

You can close a stream by adding a new endpoint to close the stream after receiving data.

Example: https://github.com/abberdeen/Streaming/blob/a5e5f5d0b64ff563b457d471204aee8112ecb9c7/StreamAPI/Controllers/StreamController.cs#L45

Learn more about issues related to streaming requests:

https://web.dev/i18n/ru/fetch-upload-streaming/

NOTE: When HTTPS is enabled on the server, the main problem that causes the error (net::ERR_HTTP2_PROTOCOL_ERROR) may be hiding, so you definitely need to check for HTTP (that is, disable HTTPS or just run on HTTP).

Cause & Solution #2: CORS

What could be the cause of the error?

There is only one reason for this error: CORS restrictions are causing the issue.

Why does it work in the browser but not in your code? Because from the browser you are directly accessing the API domain. And when you execute a request from Blazor WASM, the request is executed from the Blazor WASM domain.

In short, your Blazor WASM app is accessing an API domain from a different domain, which is not allowed by your API.

How can I find the actual error that is happening?

  • If you don't see the value in Response Headers Access-Control-Allow-Origin: * or Access-Control-Allow-Origin: yourdomain.com this is CORS issue
  • If you see this error in the browser console logs, then this is CORS:
Access to fetch at 'https://yourapidomain.com/yourendpoint' from origin 'https://localhost:1234' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

What are the ways to solve this issue?

  • First host your API and Blazor WASM on the same domain.
  • If you cannot place it in one domain, the second and only correct way is to allow your API in the CORS settings to execute a request for all domains or specifically for your Blazor WASM application domain.

If you want to choose the second way, then in your API:

Is there a way to bypass CORS restrictions without changing the code?

  • Hackers are still looking for this way.

There are other ways you can come up with different perversion ways, but they will not be effective and will not be accepted in a decent society:

  • Using a web browser component (CefSharp), I've tried ReverseProxy but it is not available in some cases, you can try Iframe, you can try to write your code in pure WASM, etc.

Learn more:

P.S. You can make shorter your code

Stream responseStream = await http.GetStreamAsync(path);
using (StreamReader reader = new StreamReader(responseStream))
{
   text = await reader.ReadToEndAsync();
   Console.Write(text);
}
abberdeen
  • 323
  • 7
  • 32
  • You may be right in your analysis but probably not the solution. As mentioned, this web app is already calling that same API with hundreds of different calls. Only this streaming response endpoint fails. Perhaps that is because the current configuration of CORS in Azure is not being utilized when giving a stream response instead of a normal IActionResult. – Niels Brinch Mar 17 '23 at 11:47
  • 1
    @NielsBrinch please take a look to update – abberdeen Mar 18 '23 at 07:00
  • I have looked at the input. I cannot disable https on server side. But when I run it on local host it’s without https and here is the same issue – Niels Brinch Mar 19 '23 at 13:17
  • As mentioned in another comment, the ERR_INCOMPLETE_CHUNKED_ENCODING error is even coming when opening the URL in the address bar, so it turns out its probably not CORS related. – Niels Brinch Mar 19 '23 at 19:17
1

Cause & Solution #3:

You need to explicitly set the Content-Length header for the Response to prevent ASP.NET from chunking the response on flushing.

Minimal reproduction of the problem: https://github.com/abberdeen/Streaming


        [FunctionName("RespondWithStream")]
        public static async Task Run([HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "stream-text")] HttpRequest req, ILogger log)
        {
            try
            {
                List list = new List
                {
                   "text1",
                   "text2",
                   "text3"
                };

                var response = req.HttpContext.Response;
                response.StatusCode = 200;
                response.ContentType = "application/json";
                response.Headers.Add("Access-Control-Allow-Origin", "*");
                response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
                response.Headers.Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, photish-tokenkey");
                response.Headers.Add("Access-Control-Allow-Credentials", "true");

                int totalByteLength = list.Sum(x => Encoding.UTF8.GetBytes(x).Length);
                response.Headers.Add("Content-Length", totalByteLength.ToString());

                StringBuilder sb = new();
                await using var sw = new StreamWriter(response.Body);
                foreach (var item in list)
                {
                    sb.Append(item);
                    await sw.WriteAsync(item);
                    await sw.FlushAsync();

                    Thread.Sleep(2000);
                }
            }
            catch (Exception exc)
            {
                log.LogError(exc, "Issue when streaming: " + exc.Message);
            }
        }
    }
Rex
  • 11
  • 1
  • When streaming a response, it is completely acceptable to set the ContentLength to null or -1 to indicate the length is unknown. – Niels Brinch Mar 20 '23 at 08:45
  • Issue when streaming: Invalid Content-Length: "-1". Value must be a positive integral number. Microsoft.AspNetCore.Server.Kestrel.Core: Invalid Content-Length: "-1". Value must be a positive integral number. – Rex Mar 20 '23 at 10:27
  • You are right. However, in older classes in .NET you would indicate "null" in Content-Length by inputting "-1" as the type was ´long´ and not nullable. – Niels Brinch Mar 21 '23 at 10:06