unosquare / embedio

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

HttpContext.Response.Headers.Clear() does not clear headers. #505

Open simontuffley opened 3 years ago

simontuffley commented 3 years ago

Describe the bug I am trying to send an image to a web browser. But I cannot clear the headers so that the Content-Type is just image/jpeg.

To Reproduce

[Route(HttpVerbs.Get, "/rooms/{roomId?}")]
public string GetRoomTile(ushort roomId)
{
    try
    {
        HttpContext.Response.Headers.Clear();
        HttpContext.Response.Headers.Add("Content-Type: image/jpeg");

        var img = _fileSystem.Read($"\\RM\\Data\\Rooms\\{roomId}.jpg");

        return img;
    }
    catch (Exception e)
    {
        Console.WriteLine(e.Message);
        throw new Exception("Error");
    }
}

This is the response header I get though:

Cache-Control: no-store, no-cache, must-revalidate
Connection: close
Content-Encoding: gzip
Content-Type: image/jpeg,application/json; charset=utf-8
Date: Thu, 04 Mar 2021 18:13:25 GMT
Expires: Sat, 26 Jul 1997 05:00:00 GMT
Last-Modified: Thu, 04 Mar 2021 18:13:25 GMT
Pragma: no-cache
Server: EmbedIO/3.4.3
Vary: Accept-Encoding

Expected behavior

Content-Type: image/jpeg
bdurrer commented 3 years ago

context.Response.ContentType = "image/jpeg"

Headers.Add() does not replace, it appends to the headers. Set would be better suited for that job

simontuffley commented 3 years ago

Hi there,

Thanks for your prompt response. I have changed the code as suggested but I still don't get the expected response:

        [Route(HttpVerbs.Get, "/rooms/{roomId?}")]
        public string GetRoomTile(ushort roomId)
        {
            try
            {
                HttpContext.Response.ContentType = "image/jpeg";

                var img = _fileSystem.Read($"\\RM\\Data\\Rooms\\{roomId}.jpg");

                return img;
            }
            catch (FileNotFoundException ioEx)
            {
                Console.WriteLine(ioEx.Message);
                throw new Exception("Error");
            }
        }
Cache-Control: no-store, no-cache, must-revalidate
Connection: close
Content-Encoding: gzip
Content-Type: application/json; charset=utf-8
Date: Thu, 04 Mar 2021 20:10:45 GMT
Expires: Sat, 26 Jul 1997 05:00:00 GMT
Last-Modified: Thu, 04 Mar 2021 20:10:44 GMT
Pragma: no-cache
Server: EmbedIO/3.4.3
Vary: Accept-Encoding
rdeago commented 3 years ago

Hello @simontuffley, thanks for using EmbedIO!

As the name implies, WebApiModule was written to serve web APIs, not files. Anything you return from a web API controller method is automatically serialized in JSON format, hence the change in Content-Type.

It is possible to change the serialization method used by a WebApiModule; just be aware that all methods of all controllers associated with the module will use the same serializer.

EmbedIO does not currently offer any other ready-made serializer other than the default (with a couple variants to customize JSON output). The upcoming version 3.5.0 will have a "do-nothing" serializer, though, that will let you set your content type and return any data to the client untouched. More details in the relevant PR.

If you dont' want to wait for the 3.5.0 release, here's a quick trick you can use.

First let's define a response serializer that sends data to the client as-is. opy the following code in a CustomResponseSerializer.cs file in you application:

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

namespace YOUR_NAMESPACE
{
    /// <summary>
    /// Provides custom response serializer callbacks.
    /// </summary>
    public static class CustomResponseSerializer
    {
        private static readonly ResponseSerializerCallback ChunkedEncodingBaseSerializer = GetBaseSerializer(false);
        private static readonly ResponseSerializerCallback BufferingBaseSerializer = GetBaseSerializer(true);

        /// <summary>
        /// Sends data in a HTTP response without serialization.
        /// </summary>
        /// <param name="bufferResponse"><see langword="true"/> to write the response body to a memory buffer first,
        /// then send it all together with a <c>Content-Length</c> header; <see langword="false"/> to use chunked
        /// transfer encoding.</param>
        /// <returns>A <see cref="ResponseSerializerCallback"/> that can be used to serialize data to a HTTP response.</returns>
        /// <remarks>
        /// <para><see cref="string"/>s and one-dimensional arrays of <see cref="byte"/>s
        /// are sent to the client unchanged; every other type is converted to a string.</para>
        /// <para>The <see cref="IHttpResponse.ContentType">ContentType</see> set on the response is used to negotiate
        /// a compression method, according to request headers.</para>
        /// <para>Strings (and other types converted to strings) are sent with the encoding specified by <see cref="IHttpResponse.ContentEncoding"/>.</para>
        /// </remarks>
        public static ResponseSerializerCallback None(bool bufferResponse)
            => bufferResponse ? BufferingBaseSerializer : ChunkedEncodingBaseSerializer;

        private static ResponseSerializerCallback GetBaseSerializer(bool bufferResponse)
            => async (context, data) => {
                if (data is null)
                {
                    return;
                }

                var isBinaryResponse = data is byte[];

                if (!context.TryDetermineCompression(context.Response.ContentType, out var preferCompression))
                {
                    preferCompression = true;
                }

                if (isBinaryResponse)
                {
                    var responseBytes = (byte[])data;
                    using var stream = context.OpenResponseStream(bufferResponse, preferCompression);
                    await stream.WriteAsync(responseBytes, 0, responseBytes.Length).ConfigureAwait(false);
                }
                else
                {
                    var responseString = data is string stringData ? stringData : data.ToString() ?? string.Empty;
                    using var text = context.OpenResponseText(context.Response.ContentEncoding, bufferResponse, preferCompression);
                    await text.WriteAsync(responseString).ConfigureAwait(false);
                }
            };
    }
}

Then change you web server initialization code to specify the new serializer:

            // Change the false parameter to true if you need to transmit a "Content-Length" header
            // (some non-browser clients need it to work properly);
            // otherwise the response will use chunked transfer encoding,
            // which is fine for browsers and will save time and memory.
            .WithWebApi("/YOUR_PATH", CustomResponseSerializer.None(false), m => m
                .WithController<YOUR_CONTROLLER_CLASS>())

Your controller method has a few issues too:

In the following code I'll assume the existence of a ReadAsByteArray method that returns byte[]:

        [Route(HttpVerbs.Get, "/rooms/{roomId}")]
        public string GetRoomTile(ushort roomId)
        {
            try
            {
                Response.ContentType = "image/jpeg";
                return _fileSystem.ReadAsByteArray($"\\RM\\Data\\Rooms\\{roomId}.jpg");
            }
            catch (FileNotFoundException ioEx)
            {
                Console.WriteLine(ioEx.Message);
                throw HttpException.NotFound();
            }
        }

I hope this helps you.

stale[bot] commented 3 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.