Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get a response from the Nancy Negotiator?

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 ???;
}
like image 546
Byron Sommardahl Avatar asked Apr 28 '14 23:04

Byron Sommardahl


2 Answers

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.

like image 92
ctrlplusb Avatar answered Sep 21 '22 15:09

ctrlplusb


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);
    };
}
like image 33
Zack Martin Avatar answered Sep 19 '22 15:09

Zack Martin