I have a NancyContext
and I need to get a Response
with a body based on the correct content negotiator for the request. I think I can use Nancy's Negotiator
class to add a model, set the status, and other things. But then, I need to return a subtype of Response
. So, what can I use to build the response using the Negotiator
?
Here's my method:
public Response ConvertToHttpResponse(Exception exception, NancyContext context)
{
var negotiator = new Negotiator(context)
.WithStatusCode(HttpStatusCode.BadRequest)
.WithReasonPhrase(exception.Message);
return ???;
}
I personally prefer to use the Nancy negotiator to return "Happy Path" results only (i.e. the view/jsondto returns), and then return vanilla nancy Response objects for any errors that may occur.
One way of doing this would be to return the errors directly within your module, for e.g.:
public class ProductsModule : NancyModule
{
public ProductsModule()
: base("/products")
{
Get["/product/{productid}"] = _ =>
{
var request = this.Bind<ProductRequest>();
var product = ProductRepository.GetById(request.ProductId);
if (product == null)
{
var error = new Response();
error.StatusCode = HttpStatusCode.BadRequest;
error.ReasonPhrase = "Invalid product identifier.";
return error;
}
var user = UserRepository.GetCurrentUser();
if (false == user.CanView(product))
{
var error = new Response();
error.StatusCode = HttpStatusCode.Unauthorized;
error.ReasonPhrase = "User has insufficient privileges.";
return error;
}
var productDto = CreateProductDto(product);
var htmlDto = new {
Product = productDto,
RelatedProducts = GetRelatedProductsDto(product)
};
return Negotiate
.WithAllowedMediaRange(MediaRange.FromString("text/html"))
.WithAllowedMediaRange(MediaRange.FromString("application/json"))
.WithModel(htmlDto) // Model for 'text/html'
.WithMediaRangeModel(
MediaRange.FromString("application/json"),
productDto); // Model for 'application/json';
}
}
}
This can get pretty messy though. My preferred approach is to set up my error handling "once" within my Nancy module bootstrapper, and have it catch known/expected exceptions and return them with the appropriate response object.
A simple example of a bootrapper configuration for this could be:
public class MyNancyBootstrapper : DefaultNancyBootstrapper
{
protected override void ApplicationStartup(
TinyIoCContainer container, IPipelines pipelines)
{
base.ApplicationStartup(container, pipelines);
// Register the custom exceptions handler.
pipelines.OnError += (ctx, err) => HandleExceptions(err, ctx); ;
}
private static Response HandleExceptions(Exception err, NancyContext ctx)
{
var result = new Response();
result.ReasonPhrase = err.Message;
if (err is NotImplementedException)
{
result.StatusCode = HttpStatusCode.NotImplemented;
}
else if (err is UnauthorizedAccessException)
{
result.StatusCode = HttpStatusCode.Unauthorized;
}
else if (err is ArgumentException)
{
result.StatusCode = HttpStatusCode.BadRequest;
}
else
{
// An unexpected exception occurred!
result.StatusCode = HttpStatusCode.InternalServerError;
}
return result;
}
}
Using this, you can refactor your module to simply throw the appropriate exception which will invoke the correct response type. You can start to create a nice set of standards for your API in this respect. An example of this would be:
public class ProductsModule : NancyModule
{
public ProductsModule()
: base("/products")
{
Get["/product/{productid}"] = _ =>
{
var request = this.Bind<ProductRequest>();
var product = ProductRepository.GetById(request.ProductId);
if (product == null)
{
throw new ArgumentException(
"Invalid product identifier.");
}
var user = UserRepository.GetCurrentUser();
if (false == user.CanView(product))
{
throw new UnauthorizedAccessException(
"User has insufficient privileges.");
}
var productDto = CreateProductDto(product);
var htmlDto = new {
Product = productDto,
RelatedProducts = GetRelatedProductsDto(product)
};
return Negotiate
.WithAllowedMediaRange(MediaRange.FromString("text/html"))
.WithAllowedMediaRange(MediaRange.FromString("application/json"))
.WithModel(htmlDto) // Model for 'text/html'
.WithMediaRangeModel(
MediaRange.FromString("application/json"),
productDto); // Model for 'application/json';
}
}
}
This feels slightly cleaner to me, and now I am introducing a set of standards into my modules. :)
Something else you could consider doing, which can be especially helpful during development would be to attach a full exception report to the Content result of your error Response objects.
A basic example of this would be:
result.Contents = responseStream =>
{
string errorBody = string.Format(
@"<html>
<head>
<title>Exception report</title>
</head>
<body>
<h1>{0}</h1>
<p>{1}</p>
</body>
</html>",
ex.Message,
ex.StackTrace);
// convert error to stream and copy to response stream
var byteArray = Encoding.UTF8.GetBytes(errorBody);
using (var errorStream = new MemoryStream(byteArray))
{
errorStream.CopyTo(responseStream);
}
}
Again, this is just a very basic, illustrative example, and you would have to decide if it is appropriate to your solution and then expand upon it.
Based on your code sample, here's one possible way:
public Response ConvertToHttpResponse(Exception exception, NancyContext context, IEnumerable<IResponseProcessor> processors, Nancy.Conventions.AcceptHeaderCoercionConventions coercionConventions)
{
var negotiator = new Negotiator(context)
.WithStatusCode(HttpStatusCode.BadRequest)
.WithReasonPhrase(exception.Message);
return new DefaultResponseNegotiator(processors, coercionConventions)
.NegotiateResponse(negotiator, context);
}
Depending on your implementation, a better way may be to have processors
and coercionConventions
as parameters to the class constructor, and allow the IoC container to resolve them as normal. However, in my case, I resolved them in my bootstrapper, and gave them to an extension method I created for negotiating Exception
instances to an XML or JSON response.
protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
// Resolving outside the lambda because no more components will be registered at this point.
var responseProcessors = container.Resolve<IEnumerable<Nancy.Responses.Negotiation.IResponseProcessor>>();
var coercionConventions = container.Resolve<AcceptHeaderCoercionConventions>();
pipelines.OnError += (context, exception) =>
{
return exception.GetErrorResponse(context, responseProcessors, coercionConventions);
};
}
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