Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save cajuncoding/a4a7b590986fd848b5040da83979796c to your computer and use it in GitHub Desktop.
Save cajuncoding/a4a7b590986fd848b5040da83979796c to your computer and use it in GitHub Desktop.
Enable Response Compression (Gzip, Brotli, Deflate) in Azure Functions In Process (Linux App Deployment)
using System;
using System.IO.Compression;
using System.Linq;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace AzureFunctionsInProcessCompressionExtensions
{
/// <summary>
/// BBernard / CajunCoding (MIT License)
/// Extension method for the HttpContext & HttpRequest to enable response compression within Azure Functions using the
/// In Process Model -- which does not support custom Middleware, so it cannot easily intercept & handle all requests.
/// There are many issues reported with response compression not always working, particularly with Linux deployments,
/// therefore this helps to easily enable this for any request by simply calling the extension method in the Function invocation.
/// It works by simply inspecting the AcceptEncoding header to determine which, if any, compression encodings are supported (Gzip, Brotli, Deflate)
/// and then wrapping the Response Body Stream with the correct implementation to encode the response while also setting the correct Response Header
/// for the Client to correctly decode the response.
///
/// NOTE: *** IMPORTANT!
/// *** You MUST handle the response writing manually to support the Compression; you cannot rely on returning
/// *** an object, or HttpMessageResult, etc... the easiest way is to simply return an IActionResult of new EmptyResult().
///
/// This works great with GraphQL via Azure Functions (In Process Model) using HotChocolate GraphQL server allowing for compression of large
/// GraphQL Query results which can significantly improve performance in use cases with large query result sets.
/// </summary>
internal static class AzureFunctionsInProcessCompressionExtensions
{
public static class AcceptCompressionNames
{
public const string Gzip = "gzip";
public const string Brotli = "br";
public const string Deflate = "deflate";
}
public static HttpRequest EnableResponseCompression(this HttpRequest httpRequest)
=> httpRequest.HttpContext.EnableResponseCompression().Request;
public static HttpContext EnableResponseCompression(this HttpContext httpContext, CompressionLevel compressionLevel = CompressionLevel.Fastest)
{
var acceptCompressionTypes = httpContext.Request.Headers.TryGetValue(HeaderNames.AcceptEncoding, out var encodings)
? encodings.SelectMany(v => v.Split(',')).Select(v => v.Trim().ToLower()).ToArray()
: Array.Empty<string>();
if (acceptCompressionTypes.Length <= 0)
return httpContext;
var httpResponse = httpContext.Response;
var responseStream = httpResponse.Body;
var responseHeaders = httpResponse.Headers;
foreach (var compressionType in acceptCompressionTypes)
{
switch (compressionType)
{
case AcceptCompressionNames.Gzip:
httpResponse.Body = new GZipStream(responseStream, compressionLevel, leaveOpen: false);
responseHeaders[HeaderNames.ContentEncoding] = new StringValues(AcceptCompressionNames.Gzip);
return httpContext;
case AcceptCompressionNames.Brotli:
httpResponse.Body = new BrotliStream(responseStream, compressionLevel, leaveOpen: false);
responseHeaders[HeaderNames.ContentEncoding] = new StringValues(AcceptCompressionNames.Brotli);
return httpContext;
case AcceptCompressionNames.Deflate:
httpResponse.Body = new DeflateStream(responseStream, compressionLevel, leaveOpen: false);
responseHeaders[HeaderNames.ContentEncoding] = new StringValues(AcceptCompressionNames.Deflate);
return httpContext;
}
}
return httpContext;
}
}
}
@cajuncoding
Copy link
Author

cajuncoding commented Mar 15, 2024

Example implementation with HotChocolate GraphQL Azure Function (Az Func v4 with Linux Deployment):

using System;
using System.Threading.Tasks;
using AzureFunctionsInProcessCompressionExtensions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;
using Microsoft.AspNetCore.Http;
using HotChocolate.AzureFunctions;
using Microsoft.Extensions.Logging;

namespace AzFuncGraphQLInProcess
{
    public class GraphQLFunctionApiEndpoint
    {
        private readonly IGraphQLRequestExecutor _graphqlExecutor;

        public GraphQLFunctionApiEndpoint(IGraphQLRequestExecutor graphqlExecutor)
        {
            _graphqlExecutor = graphqlExecutor;
        }

        [FunctionName(nameof(GraphQLFunctionApiEndpoint))]
        public Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "graphql")] HttpRequest req, ILogger logger)
            => _graphqlExecutor.ExecuteAsync(req.EnableResponseCompression()); //<==Easy inline implementation of Compression Handling!
    }
}

@cajuncoding
Copy link
Author

cajuncoding commented Mar 15, 2024

A standard example (non-GraphQL) of writing to the compressed response from a Linux deployed Azure Function is:

[FunctionName(nameof(MyHttpTriggerWithResponseCompressionSuppport))]
public Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = "compressed-response-api")] HttpRequest req, ILogger logger)
{
    req.EnableResponseCompression(); //<== Enable Response Compression BEFORE writing Anything to the response!

    var httpResponse = req.HttpContext.Response;
    httpResponse.StatusCode = (int)HttpStatusCode.OK;
    httpResponse.Headers.ContentType = MediaTypeNames.Application.Json;

    //DO some work and write to the HttpContext Response Body stream... all content will be compressed...
    await using var responseWriter = new StreamWriter(httpResponse.Body, Encoding.UTF8, leaveOpen: true);
    await responseWriter.WriteAsync(JsonSerializer.Serialize<object>(new
    {
        Topic = "Hello World...I'm Compressed!",
        Body = "If I was some big JSON then I'd be nicely packed for transmission!"
    }));

    return new EmptyResult();
}

@cajuncoding
Copy link
Author

And if anyone is interested on additional information on this issue of default Gzip compression not being supported on Linux deployments (but it is on Windows deployments) check out this (still) open Github issue: Azure/azure-functions-host#7285

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment