Reputation: 12025
I need to implement an endpoint that can accept a POST message with a gzip-compressed request body (not, not a compressed response body).
I found a way to handle this pretty easily by marking the request DTO with IRequiresRequestStream
, and using GZipStream
in the service class to decompress the raw request body and construct a string representing the decompressed data. It's a plain text request body, so in this case there's nothing lost by skipping automatic request DTO deserialization.
This is fine, I could stop there, but
I was thinking about whether I could write this in a generic way so that every request posted to ServiceStack could be decompressed if it has the Content-Encoding
header set correctly. I wrote a PreRequestFilter
that was looking pretty good, until I ran into a problem: I can't find a way to modify the original request input stream or otherwise ensure that deserialization and other code later in the pipeline can access the decompressed bytes instead of the compressed data:
public static void Decompress(IRequest request, IResponse response)
{
if (!request.Headers[HttpHeaders.ContentEncoding].EqualsIgnoreCase(CompressionTypes.GZip))
return;
request.UseBufferedStream = true;
using (var decompressor = new GZipStream(request.InputStream, CompressionMode.Decompress))
{
var decompressedBytes = decompressor.ReadFully();
// Fails because the MemoryStream, used when UseBufferedStream
// is true, is not expandable.
// Or, if we set request.UseBufferedStream = false first, then
// it fails because the original HttpInputStream is not writeable.
request.InputStream.SetLength(decompressedBytes.Length);
request.InputStream.Write(decompressedBytes, 0, decompressedBytes.Length);
}
}
PreRequestFilters.Add(Decompress);
Is there any way to modify the request body in a PreRequestFilter or RawHttpHandler? Or alternatively, is there a totally different way to implement generic request body decompression for any content type/DTO? The per-request implementation I have working now is fine but it would be interesting to find a general solution.
Upvotes: 3
Views: 758
Reputation: 143359
You can override AppHost.OnPreExecuteServiceFilter()
to change what Request DTO is used to execute your Services with, e.g:
class AppHost : AppHostBase
{
public virtual object OnPreExecuteServiceFilter(IService service,
object request, IRequest httpReq, IResponse httpRes)
{
if (httpReq.Headers[HttpHeaders.ContentEncoding]
.EqualsIgnoreCase(CompressionTypes.GZip))
{
//...
return customRequest;
}
return request;
}
}
Since this is something that can be genericized at the outer HTTP Protocol layer, an even better option would be to use generic a GZip HttpModule, e.g:
public class GZipRequestDecompressingModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.BeginRequest += (sender, e) =>
{
var request = (sender as HttpApplication).Request;
string contentEncoding = request.Headers["Content-Encoding"];
if (string.Equals(contentEncoding, "gzip",
StringComparison.OrdinalIgnoreCase))
{
request.Filter = new GZipStream(request.Filter,
CompressionMode.Decompress);
request.Headers.Remove("Content-Encoding");
}
};
}
public void Dispose()
{
}
}
Which can be configured in your Web.Config with:
<system.webServer>
<modules runAllManagedModulesForAllRequests="true">
<add name="AnyUniqueName"
type="YourNamespace.GZipRequestDecompressingModule, YourAssembly"
preCondition="integratedMode" />
</modules>
</system.webServer>
Upvotes: 3