Examples

Some examples that may prove potentially useful for those using HTTP.jl. The code for these examples can also be found on Github in the docs/examples folder.

Simple Server

A simple example of creating a server with HTTP.jl. It handles creating, deleting, updating, and retrieving Animals from a dictionary through 4 different routes

using HTTP, JSON3, StructTypes, Sockets

# modified Animal struct to associate with specific user
mutable struct Animal
    id::Int
    userId::Base.UUID
    type::String
    name::String
    Animal() = new()
end

StructTypes.StructType(::Type{Animal}) = StructTypes.Mutable()

# use a plain `Dict` as a "data store"
const ANIMALS = Dict{Int, Animal}()
const NEXT_ID = Ref(0)
function getNextId()
    id = NEXT_ID[]
    NEXT_ID[] += 1
    return id
end

# "service" functions to actually do the work
function createAnimal(req::HTTP.Request)
    animal = JSON3.read(req.body, Animal)
    animal.id = getNextId()
    ANIMALS[animal.id] = animal
    return HTTP.Response(200, JSON3.write(animal))
end

function getAnimal(req::HTTP.Request)
    animalId = HTTP.getparams(req)["id"]
    animal = ANIMALS[parse(Int, animalId)]
    return HTTP.Response(200, JSON3.write(animal))
end

function updateAnimal(req::HTTP.Request)
    animal = JSON3.read(req.body, Animal)
    ANIMALS[animal.id] = animal
    return HTTP.Response(200, JSON3.write(animal))
end

function deleteAnimal(req::HTTP.Request)
    animalId = HTTP.getparams(req)["id"]
    delete!(ANIMALS, animalId)
    return HTTP.Response(200)
end

# define REST endpoints to dispatch to "service" functions
const ANIMAL_ROUTER = HTTP.Router()
HTTP.register!(ANIMAL_ROUTER, "POST", "/api/zoo/v1/animals", createAnimal)
# note the use of `*` to capture the path segment "variable" animal id
HTTP.register!(ANIMAL_ROUTER, "GET", "/api/zoo/v1/animals/{id}", getAnimal)
HTTP.register!(ANIMAL_ROUTER, "PUT", "/api/zoo/v1/animals", updateAnimal)
HTTP.register!(ANIMAL_ROUTER, "DELETE", "/api/zoo/v1/animals/{id}", deleteAnimal)

server = HTTP.serve!(ANIMAL_ROUTER, Sockets.localhost, 8080)

# using our server
x = Animal()
x.type = "cat"
x.name = "pete"
# create 1st animal
resp = HTTP.post("http://localhost:8080/api/zoo/v1/animals", [], JSON3.write(x))
x2 = JSON3.read(resp.body, Animal)
# retrieve it back
resp = HTTP.get("http://localhost:8080/api/zoo/v1/animals/$(x2.id)")
x3 = JSON3.read(resp.body, Animal)

# close the server which will stop the HTTP server from listening
close(server)
@assert istaskdone(server.task)

Cors Server

Server example that takes after the simple server, however, handles dealing with CORS preflight headers when dealing with more than just a simple request. For CORS details, see e.g. https://cors-errors.info/


using HTTP, JSON3, StructTypes, Sockets, UUIDs

# modified Animal struct to associate with specific user
mutable struct Animal
    id::Int
    userId::UUID
    type::String
    name::String
    Animal() = new()
end

StructTypes.StructType(::Type{Animal}) = StructTypes.Mutable()

# use a plain `Dict` as a "data store", outer Dict maps userId to user-specific Animals
const ANIMALS = Dict{UUID, Dict{Int, Animal}}()
const NEXT_ID = Ref(0)
function getNextId()
    id = NEXT_ID[]
    NEXT_ID[] += 1
    return id
end

# CORS preflight headers that show what kinds of complex requests are allowed to API
const CORS_OPT_HEADERS = [
    "Access-Control-Allow-Origin" => "*",
    "Access-Control-Allow-Headers" => "*",
    "Access-Control-Allow-Methods" => "POST, GET, OPTIONS"
]

# CORS response headers that set access right of the recepient
const CORS_RES_HEADERS = ["Access-Control-Allow-Origin" => "*"]

#= 
JSONMiddleware minimizes code by automatically converting the request body
to JSON to pass to the other service functions automatically. JSONMiddleware
recieves the body of the response from the other service funtions and sends
back a success response code
=#
function JSONMiddleware(handler)
    # Middleware functions return *Handler* functions
    return function(req::HTTP.Request)
        # first check if there's any request body
        if isempty(req.body)
            # we slightly change the Handler interface here because we know
            # our handler methods will either return nothing or an Animal instance
            ret = handler(req)
        else
            # replace request body with parsed Animal instance
            req.body = JSON3.read(req.body, Animal)
            ret = handler(req)
        end
        # return a Response, if its a response already (from 404 and 405 handlers)
        if ret isa HTTP.Response
            return ret
        else # otherwise serialize any Animal as json string and wrap it in Response
            return HTTP.Response(200, CORS_RES_HEADERS, ret === nothing ? "" : JSON3.write(ret))
        end
    end
end

#= CorsMiddleware: handles preflight request with the OPTIONS flag
If a request was recieved with the correct headers, then a response will be 
sent back with a 200 code, if the correct headers were not specified in the request,
then a CORS error will be recieved on the client side

Since each request passes throught the CORS Handler, then if the request is 
not a preflight request, it will simply go to the JSONMiddleware to be passed to the
correct service function =#
function CorsMiddleware(handler)
    return function(req::HTTP.Request)
        if HTTP.method(req)=="OPTIONS"
            return HTTP.Response(200, CORS_OPT_HEADERS)
        else 
            return handler(req)
        end
    end
end

# **simplified** "service" functions
function createAnimal(req::HTTP.Request)
    animal = req.body
    animal.id = getNextId()
    ANIMALS[animal.userId][animal.id] = animal
    return animal
end

function getAnimal(req::HTTP.Request)
    # retrieve our matched path parameters from registered route
    animalId = parse(Int, HTTP.getparams(req)["id"])
    userId = UUID(HTTP.getparams(req)["userId"])
    return ANIMALS[userId][animalId]
end

function updateAnimal(req::HTTP.Request)
    animal = req.body
    ANIMALS[animal.userId][animal.id] = animal
    return animal
end

function deleteAnimal(req::HTTP.Request)
    # retrieve our matched path parameters from registered route
    animalId = parse(Int, HTTP.getparams(req)["id"])
    userId = UUID(HTTP.getparams(req)["userId"])
    delete!(ANIMALS[userId], animal.id)
    return nothing
end

function createUser(req::HTTP.Request)
    userId = uuid4()
    ANIMALS[userId] = Dict{Int, Animal}()
    return userId
end

# CORS handlers for error responses
cors404(::HTTP.Request) = HTTP.Response(404, CORS_RES_HEADERS, "")
cors405(::HTTP.Request) = HTTP.Response(405, CORS_RES_HEADERS, "")

# add an additional endpoint for user creation
const ANIMAL_ROUTER = HTTP.Router(cors404, cors405)
HTTP.register!(ANIMAL_ROUTER, "POST", "/api/zoo/v1/users", createUser)
# modify service endpoints to have user pass UUID in
HTTP.register!(ANIMAL_ROUTER, "POST", "/api/zoo/v1/users/{userId}/animals", createAnimal)
HTTP.register!(ANIMAL_ROUTER, "GET", "/api/zoo/v1/users/{userId}/animals/{id}", getAnimal)
HTTP.register!(ANIMAL_ROUTER, "DELETE", "/api/zoo/v1/users/{userId}/animals/{id}", deleteAnimal)

server = HTTP.serve!(ANIMAL_ROUTER |> JSONMiddleware |> CorsMiddleware, Sockets.localhost, 8080)

# using our server
resp = HTTP.post("http://localhost:8080/api/zoo/v1/users")
userId = JSON3.read(resp.body, UUID)
x = Animal()
x.userId = userId
x.type = "cat"
x.name = "pete"
# create 1st animal
resp = HTTP.post("http://localhost:8080/api/zoo/v1/users/$(userId)/animals", [], JSON3.write(x))
x2 = JSON3.read(resp.body, Animal)
# retrieve it back
resp = HTTP.get("http://localhost:8080/api/zoo/v1/users/$(userId)/animals/$(x2.id)")
x3 = JSON3.read(resp.body, Animal)
# try bad path
resp = HTTP.get("http://localhost:8080/api/zoo/v1/badpath")

# close the server which will stop the HTTP server from listening
close(server)
@assert istaskdone(server.task)

Server Sent Events

Simple server that implements server-sent events, loosely following this tutorial.

Example client code (JS):

<html>
<head>
    <meta charset="UTF-8">
    <title>Server-sent events demo</title>
</head>
<body>
    <h3>Fetched items:</h3>
    <ul id="list"></ul>
</body>
<script>
    const evtSource = new EventSource("http://127.0.0.1:8080/api/events")
    evtSource.onmessage = async function (event) {
        const newElement = document.createElement("li");
        const eventList = document.getElementById("list");
        if (parseFloat(event.data) > 0.5) {
            const r = await fetch("http://127.0.0.1:8080/api/getItems")
            if (r.ok) {
                const body = await r.json()
                newElement.textContent = body;
                eventList.appendChild(newElement);
            }
        }
    }
    evtSource.addEventListener("ping", function(event) {
        console.log('ping:', event.data)
    });
</script>
</html>

Example client code (Julia)

using HTTP, JSON

HTTP.open("GET", "http://127.0.0.1:8080/api/events") do io
    while !eof(io)
        println(String(readavailable(io)))
    end
end

Server code:

using HTTP, Sockets, JSON

const ROUTER = HTTP.Router()

function getItems(req::HTTP.Request)
    headers = [
        "Access-Control-Allow-Origin" => "*",
        "Access-Control-Allow-Methods" => "GET, OPTIONS"
    ]
    if HTTP.method(req) == "OPTIONS"
        return HTTP.Response(200, headers)
    end
    return HTTP.Response(200, headers, JSON.json(rand(2)))
end

function events(stream::HTTP.Stream)
    HTTP.setheader(stream, "Access-Control-Allow-Origin" => "*")
    HTTP.setheader(stream, "Access-Control-Allow-Methods" => "GET, OPTIONS")
    HTTP.setheader(stream, "Content-Type" => "text/event-stream")

    if HTTP.method(stream.message) == "OPTIONS"
        return nothing
    end

    HTTP.setheader(stream, "Content-Type" => "text/event-stream")
    HTTP.setheader(stream, "Cache-Control" => "no-cache")
    while true
        write(stream, "event: ping\ndata: $(round(Int, time()))\n\n")
        if rand(Bool)
            write(stream, "data: $(rand())\n\n")
        end
        sleep(1)
    end
    return nothing
end

HTTP.register!(ROUTER, "GET", "/api/getItems", HTTP.streamhandler(getItems))
HTTP.register!(ROUTER, "/api/events", events)

server = HTTP.serve!(ROUTER, "127.0.0.1", 8080; stream=true)

# Julia usage
resp = HTTP.get("http://localhost:8080/api/getItems")

close = Ref(false)
@async HTTP.open("GET", "http://127.0.0.1:8080/api/events") do io
    while !eof(io) && !close[]
        println(String(readavailable(io)))
    end
end

# run the following to stop the streaming client request
close[] = true

# close the server which will stop the HTTP server from listening
close(server)
@assert istaskdone(server.task)

Session

A simple example of creating a persistent session and logging into a web form. HTTP.jl does not have a distinct session object like requests.session() or rvest::html_session() but rather uses the cookies flag along with standard functions

using HTTP

#dummy site, any credentials work
url = "http://quotes.toscrape.com/login"
session = HTTP.get(url; cookies = true)

credentials = Dict(
    "Username" => "username",
    "Password" => "password")

response = HTTP.post(url, credentials)

Squaring Server Client

Simple server in Julia and client code in JS.

Example client code (JS):

<html>
<head>
    <meta charset="UTF-8">
    <title>Squaring numbers</title>
</head>
<body>
    <input id="number" placeholder="Input a number" type="number">
    <button id="submit">Square</button>
    <h4>Outputs</h4>
    <ul id="list"></ul>
</body>
<script>
    document.getElementById('submit').addEventListener('click', async function (event) {
        const list = document.getElementById('list');
        try {
            const r = await fetch('http://127.0.0.1:8080/api/square', {
                method: 'POST',
                body: document.getElementById('number').value
            });

            if (r.ok) {
                const body = await r.text()
                const newElement = document.createElement('li');
                newElement.textContent = body;
                list.insertBefore(newElement, list.firstChild);
            } else {
                console.error(r)
            };
        } catch (err) {
            console.error(err)
        }
    })
</script>
</html>

Server code:

using HTTP

const ROUTER = HTTP.Router()

function square(req::HTTP.Request)
    headers = [
        "Access-Control-Allow-Origin" => "*",
        "Access-Control-Allow-Methods" => "POST, OPTIONS"
    ]
    # handle CORS requests
    if HTTP.method(req) == "OPTIONS"
        return HTTP.Response(200, headers)
    end
    body = parse(Float64, String(req.body))
    square = body^2
    HTTP.Response(200, headers, string(square))
end

HTTP.register!(ROUTER, "POST", "/api/square", square)

server = HTTP.serve!(ROUTER, Sockets.localhost, 8080)

# usage
resp = HTTP.post("http://localhost:8080/api/square"; body="3")
sq = parse(Float64, String(resp.body))
@assert sq == 9.0

# close the server which will stop the HTTP server from listening
close(server)
@assert istaskdone(server.task)

Readme Examples

#CLIENT

#HTTP.request sends a HTTP Request Message and returns a Response Message.

r = HTTP.request("GET", "http://httpbin.org/ip")
println(r.status) 
println(String(r.body)) 

#HTTP.open sends a HTTP Request Message and opens an IO stream from which the Response can be read.
HTTP.open(:GET, "https://tinyurl.com/bach-cello-suite-1-ogg") do http
    open(`vlc -q --play-and-exit --intf dummy -`, "w") do vlc
        write(vlc, http)
    end
end

#SERVERS

#Using HTTP.Servers.listen:
#The server will start listening on 127.0.0.1:8081 by default.

using HTTP

HTTP.listen() do http::HTTP.Stream
    @show http.message
    @show HTTP.header(http, "Content-Type")
    while !eof(http)
        println("body data: ", String(readavailable(http)))
    end
    HTTP.setstatus(http, 404)
    HTTP.setheader(http, "Foo-Header" => "bar")
    HTTP.startwrite(http)
    write(http, "response body")
    write(http, "more response body")
end

#Using HTTP.Handlers.serve:

using HTTP

# HTTP.listen! and HTTP.serve! are the non-blocking versions of HTTP.listen/HTTP.serve
server = HTTP.serve!() do request::HTTP.Request
    @show request
    @show request.method
    @show HTTP.header(request, "Content-Type")
    @show request.body
    try
        return HTTP.Response("Hello")
    catch e
        return HTTP.Response(400, "Error: $e")
    end
 end
 # HTTP.serve! returns an `HTTP.Server` object that we can close manually
 close(server)

#WebSocket Examples
using HTTP.WebSockets
server = WebSockets.listen!("127.0.0.1", 8081) do ws
        for msg in ws
            send(ws, msg)
        end
    end

WebSockets.open("ws://127.0.0.1:8081") do ws
           send(ws, "Hello")
           s = receive(ws)
           println(s)
       end;
Hello
#Output: Hello

close(server)