Error Handling

Live on this server

JSON errors for the API, HTML error pages for the website, custom mapping.

A shop with a website for people and an API for programs, each reporting errors in its own way: the API answers with JSON (or the format the client asks for), the website with HTML error pages, such as the 404 for an address that does not exist.

The business logic throws an OutOfStockException without knowing about HTTP, and OutOfStockMapper turns it into a 409 response. A ProviderException brings its status with it, as for an order that does not exist.

To run it, create a console app (dotnet new console), add GenHTTP (dotnet add package GenHTTP.Full), replace Program.cs with the code below and start it with dotnet run - it answers at http://localhost:8080/.

Program.cs64 lines
using GenHTTP.Engine.Internal;

using GenHTTP.Api.Content;
using GenHTTP.Api.Protocol;

using GenHTTP.Modules.ErrorHandling;
using GenHTTP.Modules.Functional;
using GenHTTP.Modules.IO;
using GenHTTP.Modules.Layouting;
using GenHTTP.Modules.Practices;

var orders = Inline.Create()
                   // GET /api/orders/42 answers 404 as JSON
                   .Get(":id", (int id) => (id == 1) ? new Order(1, "Coffee") : throw new ProviderException(ResponseStatus.NotFound, $"There is no order with id {id}"))
                   // POST /api/orders/ answers 409 via the mapper below
                   .Post((Order order) => Shop.PlaceOrder(order))
                   .Add(ErrorHandler.From(new OutOfStockMapper()));

var website = Layout.Create()
                    .Index(Content.From(Resource.FromString("<h1>Welcome to our shop!</h1>").Type(ContentType.TextHtml)))
                    .Add(ErrorHandler.Html()); // GET /does-not-exist answers 404 as HTML page

var app = Layout.Create()
                .Add("api/orders", orders)
                .Add(website);

await Host.Create()
          .Handler(app)
          .Defaults()
          .RunAsync();

public record Order(int Id, string Product);

public class OutOfStockException(string product) : Exception($"Sorry, '{product}' is sold out")
{

    public string Product => product;

}

public static class Shop
{

    public static Order PlaceOrder(Order order) => throw new OutOfStockException(order.Product);

}

public class OutOfStockMapper : IErrorMapper<OutOfStockException>
{

    public ValueTask<IResponse?> Map(IRequest request, IHandler handler, OutOfStockException error, ByteString? acceptedFormat)
    {
        var response = request.Respond()
                              .Status(ResponseStatus.Conflict)
                              .Content(error.Message, ContentType.TextPlain)
                              .Build();

        return new(response);
    }

    // null passes the request on, so the default error handling renders the 404
    public ValueTask<IResponse?> GetNotFound(IRequest request, IHandler handler, ByteString? acceptedFormat) => new();

}

Try it

Send the requests to the sample running on this server - change the path, headers or body as you like, or copy them as curl commands.

  1. Fetch an order

    GET
    curl -i 'https://genhttp.org/demo/errors/api/orders/1'
  2. Fetch an order that does not exist

    404 as JSON

    GET
    curl -i 'https://genhttp.org/demo/errors/api/orders/42'
  3. Order something that is sold out

    409, mapped from a custom exception

    POST
    Headers and body
    curl -i -X POST -H 'Content-Type: application/json' -d '{ "product": "Tea" }' 'https://genhttp.org/demo/errors/api/orders/'
  4. Open a page that does not exist

    404 as HTML page

    PAGE /demo/errors/does-not-exist Open