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).
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.
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());
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With