Serving web apps

This tutorial was generated using Literate.jl. Download the source as a .jl file.

This tutorial demonstrates how to set up and serve JuMP models via a REST API using HTTP.jl and JSON.jl. The example service accepts an HTTP POST request with a JSON body, solves a parameterized mixed-integer program, and returns the solution as JSON.

Learning intentions:

  • Structure a JuMP solver as a self-contained endpoint function that validates JSON input, builds and solves a model, and returns results as a dictionary
  • Wrap the endpoint in an HTTP request handler that parses JSON, runs the solver in a separate thread, and returns an HTTP response
  • Register the endpoint on an HTTP router, start the server, and send test requests to verify the full round-trip

Required packages

This tutorial uses the following packages:

using JuMPimport HiGHSimport HTTPimport JSON

The server side

The core components of our REST server are endpoints. These are functions which accept a Dict{String,Any} of input parameters, and return a Dict{String,Any} as output. The types are Dict{String,Any} because we're going to read these to and from JSON.

Here's a very simple endpoint: it accepts params as input, formulates and solves a trivial mixed-integer program, and then returns a dictionary with the result.

function endpoint_solve(params::Dict{String,Any})    if !haskey(params, "lower_bound")        return Dict{String,Any}(            "status" => "failure",            "reason" => "missing lower_bound param",        )    elseif !(params["lower_bound"] isa Real)        return Dict{String,Any}(            "status" => "failure",            "reason" => "lower_bound is not a number",        )    end    model = Model(HiGHS.Optimizer)    set_silent(model)    @variable(model, x >= params["lower_bound"], Int)    optimize!(model)    ret = Dict{String,Any}(        "status" => "okay",        "termination_status" => termination_status(model),        "primal_status" => primal_status(model),    )    # Only include the `x` key if it has a value.    if primal_status(model) == FEASIBLE_POINT        ret["x"] = value(x)    end    return retend
endpoint_solve (generic function with 1 method)

When we call this, we get:

endpoint_solve(Dict{String,Any}("lower_bound" => 1.2))
Dict{String, Any} with 4 entries:
  "status"             => "okay"
  "termination_status" => OPTIMAL
  "x"                  => 2.0
  "primal_status"      => FEASIBLE_POINT
endpoint_solve(Dict{String,Any}())
Dict{String, Any} with 2 entries:
  "status" => "failure"
  "reason" => "missing lower_bound param"

We now need to turn each endpoint into a function that accepts an HTTP.Request, parses the JSON input, runs the endpoint, converts the result to JSON, and returns an HTTP.Response. In addition, the computation is handled in a separate thread, and we catch any unhandled exceptions.

function wrap_endpoint(endpoint::Function)    function serve_request(request::HTTP.Request)::HTTP.Response        task = Threads.@spawn try            input = request.body |> String |> JSON.parse            ret = convert(Dict{String,Any}, input) |> endpoint |> JSON.json            HTTP.Response(200; body = ret)        catch err            HTTP.Response(500; body = "internal error: $err")        end        return fetch(task)    endend
wrap_endpoint (generic function with 1 method)

Finally, we need an HTTP server. There are a variety of ways you can do this in HTTP.jl. Here's one way:

router = HTTP.Router()# Register other routes as neededHTTP.register!(router, "/api/solve", wrap_endpoint(endpoint_solve))server = HTTP.serve!(router, "127.0.0.1", 8080)
HTTP.Server{HTTP.Handlers.Router{typeof(HTTP.Handlers.default404), typeof(HTTP.Handlers.default405), Nothing}}("tcp", "127.0.0.1:8080", HTTP.Handlers.Router{typeof(HTTP.Handlers.default404), typeof(HTTP.Handlers.default405), Nothing}(HTTP.Handlers.default404, HTTP.Handlers.default405, Node(*), nothing), false, 0, 0, 0, 0, 1048576, 67108864, HTTP.HTTP2Settings(65535, 65535), 100, true, false, true, 128, ReentrantLock(), TCP.Listener(127.0.0.1:8080, active), Task (runnable, started) @0x00007f0e4c2c70d0, Set{HTTP._ServerConn}(), "127.0.0.1:8080", 8080, HTTP._ServerState.RUNNING)

The client side

Now that we have a server, we can send it requests via this function:

function send_request(data::Dict; endpoint::String = "solve")    ret = HTTP.request(        "POST",        # This should match the URL and endpoint we defined for our server.        "http://127.0.0.1:8080/api/$endpoint",        [            "Content-Type" => "application/json",            # This line is needed so we can run this tutorial multiple times in            # the documentation. You don't need it in practice.            "Connection" => "close",        ],        JSON.json(data),    )    if ret.status != 200        # This could happen if there are time-outs, network errors, etc.        return Dict(            "status" => "failure",            "code" => ret.status,            "body" => String(ret.body),        )    end    return JSON.parse(String(ret.body))end
send_request (generic function with 1 method)

Let's see what happens:

send_request(Dict("lower_bound" => 0))
JSON.Object{String, Any} with 4 entries:
  "primal_status"      => "FEASIBLE_POINT"
  "status"             => "okay"
  "termination_status" => "OPTIMAL"
  "x"                  => 0.0
send_request(Dict("lower_bound" => 1.2))
JSON.Object{String, Any} with 4 entries:
  "primal_status"      => "FEASIBLE_POINT"
  "status"             => "okay"
  "termination_status" => "OPTIMAL"
  "x"                  => 2.0

If we don't send a lower_bound, we get:

send_request(Dict("invalid_param" => 1.2))
JSON.Object{String, Any} with 2 entries:
  "reason" => "missing lower_bound param"
  "status" => "failure"

If we don't send a lower_bound that is a number, we get:

send_request(Dict("lower_bound" => "1.2"))
JSON.Object{String, Any} with 2 entries:
  "reason" => "lower_bound is not a number"
  "status" => "failure"

Finally, we can shutdown our HTTP server:

HTTP.forceclose(server)

Next steps

For more complicated examples relating to HTTP servers, consult the HTTP.jl documentation.

To see how you can integrate this with a larger JuMP model, read Design patterns for larger models.