ServiceStack - Using gzip/deflate compression with

2019-03-27 12:16发布

问题:

I have a ServiceStack service that compresses the response using RequestContext.ToOptimizedResult(), e.g.:

[Route("/numbers/search")]
public class FindNumbers
{
}

public object Get(FindNumbers query)
{
    var data = new List<string> { "One", "Two", "Three" };
    return RequestContext.ToOptimizedResult(data);
}

This works perfectly when issuing a request like:

GET http://myhost:13487/numbers/search.json

And is compressed as expected with the Accept-Encoding request header:

Accept-Encoding: gzip,deflate,sdch

I can also issue a JSONP request:

GET http://myhost:13487/numbers/search?callback=func

which correctly returns an application/javascript callback (uncompressed).

THE PROBLEM

When I add the Accept-Encoding request header to the JSONP request, the response is the compressed JSON data as per the original JSON request, and not a compressed application/javascript callback.

Are there any obvious reasons that I'm missing for this behaviour, or is it simply a bug in ServiceStack? My expectation would be to receive a compressed JSONP callback in the response, but I'm fairly green with JSONP and there may be a good reason for the fallback.

Note, I'm in progress of working through the ServiceStack source, but I figured I'd get this out there as more brains are better than one...

Thanks in advance

EDIT

So, I've traced the issue down the following source

https://github.com/ServiceStack/ServiceStack/blob/5d09d439cd1a13712411552e2b3ede5a71af2ee5/src/ServiceStack/Host/Handlers/GenericHandler.cs#L79

and

https://github.com/ServiceStack/ServiceStack/blob/5d09d439cd1a13712411552e2b3ede5a71af2ee5/src/ServiceStack/Host/RestHandler.cs#L107

if (doJsonp && !(response is CompressedResult))
    return httpRes.WriteToResponse(httpReq, response, (callback + "(").ToUtf8Bytes(),")".ToUtf8Bytes());

return httpRes.WriteToResponse(httpReq, response);

So if the response is a compressed result, then regardless of the requirement for JSONP via ?callback=func the response will simply contain the compressed json (in the case of the example above), which rings true with my findings above. So it looks like the jsonp callback wrapper needs to be applied earlier in the callstack.

回答1:

For those that are interested, I solved this by writing a compression plugin that intercepts the response and handles the compression outside of the service method, which is where I believe it should be done. It also addresses the JSONP issue described above.

In my opinion, compression is an orthogonal concern to the service method logic, and moving this outside of the service method as a response filter enables service to service calls to exist with inherent strong typing instead of the ugly public object MyServiceMethod(DtoType request) { } signatures for allowing arbitrary compressed/uncompressed responses. I've taken the assumption here that if the client states a valid Accept-Encoding header then the response will be compressed regardless, which I think is a fair call to make.

For now, I've opted against a pull request to ServiceStack as I see it as a major change in the approach to how the framework handles compression and would require considerable upfront discussion with the owners. This code is purely for demonstrative purposes, but I'm using it and it works very well.

Code:

public class CompressionFeature : IPlugin
{
    public void Register(IAppHost appHost)
    {
        appHost.ResponseFilters.Add((request, response, dto) =>
        {
            if (dto == null || dto is AuthResponse || dto is CompressedResult || dto is Exception) return;

            using (var serializationContext = new HttpRequestContext(request, response, dto))
            {
                if (!serializationContext.RequestAttributes.AcceptsDeflate && !serializationContext.RequestAttributes.AcceptsGzip) return;

                var serializedDto = EndpointHost.ContentTypeFilter.SerializeToString(serializationContext, dto);

                var callback = request.GetJsonpCallback();
                var isJsonpRequest = EndpointHost.Config.AllowJsonpRequests && !string.IsNullOrEmpty(callback);

                if (isJsonpRequest)
                {
                    serializedDto = (callback + "(") + serializedDto + ")";
                    serializationContext.ResponseContentType = ContentType.JavaScript;
                }

                var compressedBytes = serializedDto.Compress(serializationContext.CompressionType);
                var compressedResult = new CompressedResult(compressedBytes, serializationContext.CompressionType, serializationContext.ResponseContentType);
                response.WriteToResponse(compressedResult, serializationContext.ResponseContentType);
            }
        });
    }
}

Register the plugin in your AppHost:

appHost.Plugins.Add(new CompressionFeature());