unosquare / embedio

A tiny, cross-platform, module based web server for .NET
http://unosquare.github.io/embedio
Other
1.47k stars 176 forks source link

Content-Length header missing in response when using SendStringAsync or SendDataAsync #446

Closed Zaicon closed 4 years ago

Zaicon commented 4 years ago

Describe the bug In EmbedIO v3.3.3, when sending a response to a WebApi request, the content-length header is missing.

As a side note, in EmbedIO v2.9.2, the content-length header is sent as expected.

To Reproduce Steps to reproduce the behavior:

  1. Set up a WebServer with an WebApi controller.
  2. Send a test request to the server via Postman, etc.
  3. Respond to the request using await HttpContext.SendStringAsync("example text here", "application/json; charset=utf-8", Encoding.UTF8); or await HttpContext.SendDataAsync(new { blah = "example" });
  4. View the response headers in Postman, etc. The content-length header is missing.

Expected behavior The content-length header is sent in the response.

Screenshots EmbedIO v2.9.2: image EmbedIO v3.3.3: image

rdeago commented 4 years ago

Hello and welcome back @Zaicon!

Since EmbedIO version 3, both SendStringAsync and SendDataAsync use chunked transfer encoding. The rationale is that they can be called more than once for the same response, hence it is not possible to determine the content length in advance.

May I know why the Content-Length header is so important to you? A better knowledge of your use case might help @geoperez and me to either figure out a workaround, or devise additional extension methods to IHttpContext that do set the content length.

Zaicon commented 4 years ago

Hey, I was talking to @geoperez about this. I am using EmbedIO with a Slack application. When I used v2 and sent string responses via return await Ok("Some message here.");, everything worked perfectly. When I upgraded to v3, Slack now shows an error every time I try to respond to an incoming web request.

It looks like the response payload is exactly the same (at least on Postman); the only difference is the headers returned. Geo noticed that the content-length header is missing and asked me to open this issue.

rdeago commented 4 years ago

So, apparently Slack can't handle chunked responses, possibly along with other clients. Not all of them are browsers after all.

@Zaicon, can you put the class below in your app, use SendStringContentAsync instead of SendStringAsync, and report your results? If it works, it's going to be an addition to EmbedIO v4.0, along with similar methods for binary and serialized data.

using System;
using System.Text;
using System.Threading.Tasks;
using EmbedIO;
using EmbedIO.Utilities;

namespace YOUR_NAMESPACE_HERE
{
    /// <summary>
    /// Provides extension methods for types implementing <see cref="IHttpContext"/>.
    /// </summary>
    public static class HttpContextExtensions
    {
        /// <summary>
        /// <para>Asynchronously sends a string as response.</para>
        /// <para>This method differs from <seealso cref="EmbedIO.HttpContextExtensions.SendStringAsync"/>
        /// in that it sets the <c>Content-Length</c> header in the response, thus preventing
        /// any further output to the response stream.</para>
        /// </summary>
        /// <param name="this">The <see cref="IHttpResponse"/> interface on which this method is called.</param>
        /// <param name="content">The response content.</param>
        /// <param name="contentType">The MIME type of the content.
        /// If this parameter is <see langword="null"/> or the empty string, the content type will not be set.</param>
        /// <param name="encoding">The <see cref="Encoding"/> to use.</param>
        /// <returns>A <see cref="Task"/> representing the ongoing operation.</returns>
        /// <exception cref="NullReferenceException"><paramref name="this"/> is <see langword="null"/>.</exception>
        /// <exception cref="ArgumentNullException">
        /// <para><paramref name="content"/> is <see langword="null"/>.</para>
        /// <para>- or -</para>
        /// <para><paramref name="encoding"/> is <see langword="null"/>.</para>
        /// </exception>
        public static Task SendStringContentAsync(
            this IHttpContext @this,
            string content,
            string? contentType,
            Encoding encoding)
        {
            content = Validate.NotNull(nameof(content), content);
            encoding = Validate.NotNull(nameof(encoding), encoding);

            if (!string.IsNullOrEmpty(contentType))
            {
#pragma warning disable CS8601 // Possible null reference assignment - Tested with string.NotNullOrEmpty
                @this.Response.ContentType = contentType;
#pragma warning restore CS8601
                @this.Response.ContentEncoding = encoding;
            }

            var data = encoding.GetBytes(content);
            @this.Response.ContentLength64 = data.Length;
            return @this.Response.OutputStream.WriteAsync(data, 0, data.Length, @this.CancellationToken);
        }
    }
}
Zaicon commented 4 years ago

Ayyy it worked!

image

geoperez commented 4 years ago

Also with Slack?

Zaicon commented 4 years ago

Yes, Slack accepted the response and handled it as expected.

rdeago commented 4 years ago

Great!

In the meantime, I think I've found a better way to support setting Content-Length, so SendStringContentAsync will probably not be a part of EmbedIO v4.0.

Although the code above will work with version 4, I'd advise you to use out-of-the-box extension methods as much as possible, as they shield you from having to deal with the HTTP response object directly. This will minimize you code's dependency on HttpListener, minimizing required changes when EmbedIO eventually parts ways with it.

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.