Skip to content

Instantly share code, notes, and snippets.

@aaronlerch
Last active August 29, 2015 13:56
Show Gist options
  • Save aaronlerch/8916523 to your computer and use it in GitHub Desktop.
Save aaronlerch/8916523 to your computer and use it in GitHub Desktop.
Workaround for Nancy issue #1333 https://github.com/NancyFx/Nancy/issues/1333

First note

A helpful fix was included as part of PR #1335 that makes the workaround more robust. Look for a comment in the code to delineate the two ways to handle things, with and without that pull request included. (It appears the pull request was first included in Nancy in v0.22.0 which at the time of this writing is the latest version.)

The code below demonstrates how, when running Nancy via OWIN, to work around the issue reported in issue #1333 NancyFx/Nancy#1333

The important change is in NancyOwinHost.cs lines 80-94.

/// <summary>
/// ALL of this was copied directly out of Nancy.Owin.dll's source:
/// https://github.com/NancyFx/Nancy/blob/master/src/Nancy.Owin/NancyOwinHost.cs
///
/// For the SOLE PURPOSE of customizving:
/// var nancyRequestStream = new RequestStream(owinRequestBody, ExpectedLength(owinRequestHeaders), 1048576, false);
/// </summary>
namespace FunkyTown
{
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using System.Security.Cryptography.X509Certificates;
using Nancy;
using Nancy.Owin;
using Nancy.IO;
using Nancy.Helpers;
/// <summary>
/// Nancy host for OWIN hosts
/// </summary>
public class NancyOwinHost
{
private readonly Func<IDictionary<string, object>, Task> next;
private readonly NancyOptions options;
private readonly INancyEngine engine;
/// <summary>
/// The request environment key
/// </summary>
public const string RequestEnvironmentKey = "OWIN_REQUEST_ENVIRONMENT";
/// <summary>
/// Initializes a new instance of the <see cref="NancyOwinHost"/> class.
/// </summary>
/// <param name="next">Next middleware to run if necessary</param>
/// <param name="options">The nancy options that should be used by the host.</param>
public NancyOwinHost(Func<IDictionary<string, object>, Task> next, NancyOptions options)
{
this.next = next;
this.options = options;
options.Bootstrapper.Initialise();
this.engine = options.Bootstrapper.GetEngine();
}
/// <summary>
/// OWIN App Action
/// </summary>
/// <param name="environment">Application environment</param>
/// <returns>Returns result</returns>
public Task Invoke(IDictionary<string, object> environment)
{
var owinRequestMethod = Get<string>(environment, "owin.RequestMethod");
var owinRequestScheme = Get<string>(environment, "owin.RequestScheme");
var owinRequestHeaders = Get<IDictionary<string, string[]>>(environment, "owin.RequestHeaders");
var owinRequestPathBase = Get<string>(environment, "owin.RequestPathBase");
var owinRequestPath = Get<string>(environment, "owin.RequestPath");
var owinRequestQueryString = Get<string>(environment, "owin.RequestQueryString");
var owinRequestBody = Get<Stream>(environment, "owin.RequestBody");
var owinRequestHost = GetHeader(owinRequestHeaders, "Host") ?? Dns.GetHostName();
byte[] certificate = null;
if (this.options.EnableClientCertificates)
{
var clientCertificate = Get<X509Certificate>(environment, "ssl.ClientCertificate");
certificate = (clientCertificate == null) ? null : clientCertificate.GetRawCertData();
}
var serverClientIp = Get<string>(environment, "server.RemoteIpAddress");
var url = CreateUrl(owinRequestHost, owinRequestScheme, owinRequestPathBase, owinRequestPath, owinRequestQueryString);
// HACKY HACK HACK!!!
// If you are running a version of Nancy previous to 0.22.0, you need to set the memory-stream threshold
// higher than you think you'll hit, because due to another bug Nancy ignores the last boolean parameter
// telling it to never switch between memory-stream and file-stream.
// So, for versions of Nancy earlier than 0.22.0
// Override the default memory-stream threshold from something like 81920 to 1 MEGABYTE
// And hope it works out ;)
// var nancyRequestStream = new RequestStream(owinRequestBody, ExpectedLength(owinRequestHeaders), 1048576, true);
// For versions of Nancy 0.22.0 and later, it's enough to use the default threshold length
// and specify false for the last param to disable stream switching altogether,
// as the bug has been fixed and Nancy will now respect that setting.
var nancyRequestStream = new RequestStream(owinRequestBody, ExpectedLength(owinRequestHeaders), true);
var nancyRequest = new Request(
owinRequestMethod,
url,
nancyRequestStream,
owinRequestHeaders.ToDictionary(kv => kv.Key, kv => (IEnumerable<string>)kv.Value, StringComparer.OrdinalIgnoreCase),
serverClientIp,
certificate);
var tcs = new TaskCompletionSource<int>();
this.engine.HandleRequest(
nancyRequest,
StoreEnvironment(environment),
RequestComplete(environment, this.options.PerformPassThrough, this.next, tcs),
RequestErrored(tcs));
return tcs.Task;
}
/// <summary>
/// Gets a delegate to handle converting a nancy response
/// to the format required by OWIN and signals that the we are
/// now complete.
/// </summary>
/// <param name="environment">OWIN environment</param>
/// <param name="next">A delegate that represents the next stage in OWIN pipeline</param>
/// <param name="tcs">The task completion source to signal</param>
/// <param name="performPassThrough">A delegate that determines if pass through should be performed</param>
/// <returns>Delegate</returns>
private static Action<NancyContext> RequestComplete(
IDictionary<string, object> environment,
Func<NancyContext, bool> performPassThrough,
Func<IDictionary<string, object>, Task> next,
TaskCompletionSource<int> tcs)
{
return context =>
{
var owinResponseHeaders = Get<IDictionary<string, string[]>>(environment, "owin.ResponseHeaders");
var owinResponseBody = Get<Stream>(environment, "owin.ResponseBody");
var nancyResponse = context.Response;
if (!performPassThrough(context))
{
environment["owin.ResponseStatusCode"] = (int)nancyResponse.StatusCode;
if (nancyResponse.ReasonPhrase != null)
{
environment["owin.ResponseReasonPhrase"] = nancyResponse.ReasonPhrase;
}
foreach (var responseHeader in nancyResponse.Headers)
{
owinResponseHeaders[responseHeader.Key] = new[] {responseHeader.Value};
}
if (!string.IsNullOrWhiteSpace(nancyResponse.ContentType))
{
owinResponseHeaders["Content-Type"] = new[] {nancyResponse.ContentType};
}
if (nancyResponse.Cookies != null && nancyResponse.Cookies.Count != 0)
{
const string setCookieHeaderKey = "Set-Cookie";
string[] setCookieHeader = owinResponseHeaders.ContainsKey(setCookieHeaderKey)
? owinResponseHeaders[setCookieHeaderKey]
: new string[0];
owinResponseHeaders[setCookieHeaderKey] = setCookieHeader
.Concat(nancyResponse.Cookies.Select(cookie => cookie.ToString()))
.ToArray();
}
nancyResponse.Contents(owinResponseBody);
tcs.SetResult(0);
}
else
{
next(environment).WhenCompleted(t => tcs.SetResult(0), t => tcs.SetException(t.Exception));
}
context.Dispose();
};
}
/// <summary>
/// Gets a delegate to handle request errors
/// </summary>
/// <param name="tcs">Completion source to signal</param>
/// <returns>Delegate</returns>
private static Action<Exception> RequestErrored(TaskCompletionSource<int> tcs)
{
return tcs.SetException;
}
private static T Get<T>(IDictionary<string, object> env, string key)
{
object value;
return env.TryGetValue(key, out value) && value is T ? (T)value : default(T);
}
private static string GetHeader(IDictionary<string, string[]> headers, string key)
{
string[] value;
return headers.TryGetValue(key, out value) && value != null ? string.Join(",", value.ToArray()) : null;
}
private static long ExpectedLength(IDictionary<string, string[]> headers)
{
var header = GetHeader(headers, "Content-Length");
if (string.IsNullOrWhiteSpace(header))
return 0;
int contentLength;
return int.TryParse(header, NumberStyles.Any, CultureInfo.InvariantCulture, out contentLength) ? contentLength : 0;
}
/// <summary>
/// Creates the Nancy URL
/// </summary>
/// <param name="owinRequestHost">OWIN Hostname</param>
/// <param name="owinRequestScheme">OWIN Scheme</param>
/// <param name="owinRequestPathBase">OWIN Base path</param>
/// <param name="owinRequestPath">OWIN Path</param>
/// <param name="owinRequestQueryString">OWIN Querystring</param>
/// <returns></returns>
private static Url CreateUrl(
string owinRequestHost,
string owinRequestScheme,
string owinRequestPathBase,
string owinRequestPath,
string owinRequestQueryString)
{
int? port = null;
var hostnameParts = owinRequestHost.Split(':');
if (hostnameParts.Length == 2)
{
owinRequestHost = hostnameParts[0];
int tempPort;
if (int.TryParse(hostnameParts[1], out tempPort))
{
port = tempPort;
}
}
var url = new Url
{
Scheme = owinRequestScheme,
HostName = owinRequestHost,
Port = port,
BasePath = owinRequestPathBase,
Path = owinRequestPath,
Query = owinRequestQueryString,
};
return url;
}
/// <summary>
/// Gets a delegate to store the OWIN environment into the NancyContext
/// </summary>
/// <param name="environment">OWIN Environment</param>
/// <returns>Delegate</returns>
private static Func<NancyContext, NancyContext> StoreEnvironment(IDictionary<string, object> environment)
{
return context =>
{
environment["nancy.NancyContext"] = context;
context.Items[RequestEnvironmentKey] = environment;
return context;
};
}
}
}
using System;
using System.Threading;
using Nancy.Json;
using Owin;
namespace MyApp
{
public class Startup
{
public void Configuration(IAppBuilder appBuilder)
{
// Normally you'd do something like appBuilder.UseNancy(...), perhaps with a custom bootstrapper.
//appBuilder.UseNancy(new Nancy.Owin.NancyOptions{Bootstrapper = bootstrapper});
// For this workaround, we have to do the work of the UseNancy extension method
// ourselves, to ensure we can provide our own NancyOwinHost implementation
// that contains the workaround. This includes the HookDisposal method below as well.
var nancyOptions = new Nancy.Owin.NancyOptions{ Bootstrapper = bootstrapper };
HookDisposal(appBuilder, nancyOptions);
appBuilder.Use(typeof(FunkyTown.NancyOwinHost), nancyOptions);
}
private const string AppDisposingKey = "host.OnAppDisposing";
private static void HookDisposal(IAppBuilder builder, Nancy.Owin.NancyOptions nancyOptions)
{
if (!builder.Properties.ContainsKey(AppDisposingKey))
{
return;
}
var appDisposing = builder.Properties[AppDisposingKey] as CancellationToken?;
if (appDisposing.HasValue)
{
appDisposing.Value.Register(nancyOptions.Bootstrapper.Dispose);
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment