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

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

# 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(IOBuffer(HTTP.payload(req)), Animal)
    animal.id = getNextId()
    ANIMALS[animal.id] = animal
    return HTTP.Response(200, JSON3.write(animal))
end

function getAnimal(req::HTTP.Request)
    animalId = HTTP.URIs.splitpath(req.target)[5] # /api/zoo/v1/animals/10, get 10
    animal = ANIMALS[parse(Int, animalId)]
    return HTTP.Response(200, JSON3.write(animal))
end

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

function deleteAnimal(req::HTTP.Request)
    animalId = HTTP.URIs.splitpath(req.target)[5] # /api/zoo/v1/animals/10, get 10
    delete!(ANIMALS, parse(Int, animal.id))
    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/*", getAnimal)
HTTP.@register(ANIMAL_ROUTER, "PUT", "/api/zoo/v1/animals", updateAnimal)
HTTP.@register(ANIMAL_ROUTER, "DELETE", "/api/zoo/v1/animals/*", deleteAnimal)

HTTP.serve(ANIMAL_ROUTER, ip"127.0.0.1", 8080)

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

using HTTP

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

# 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

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

#= 
JSONHandler minimizes code by automatically converting the request body
to JSON to pass to the other service functions automatically. JSONHandler
recieves the body of the response from the other service funtions and sends
back a success response code
=#
function JSONHandler(req::HTTP.Request)
    # first check if there's any request body
    body = IOBuffer(HTTP.payload(req))
    if eof(body)
        # no request body
        response_body = handle(ANIMAL_ROUTER, req)
    else
        # there's a body, so pass it on to the handler we dispatch to
        response_body = handle(ANIMAL_ROUTER, req, JSON3.read(body, Animal))
    end
    return HTTP.Response(200, JSON3.write(response_body))
end

#= CorsHandler: 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 JSONHandler to be passed to the
correct service function =#
function CorsHandler(req)
    if HTTP.hasheader(req, "OPTIONS")
        return HTTP.Response(200, headers = headers)
    else 
        return JSONHandler(req)
    end


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

function getAnimal(req::HTTP.Request)
    animalId = HTTP.URIs.splitpath(req.target)[5] # /api/zoo/v1/animals/10, get 10
    return ANIMALS[animalId]
end

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

function deleteAnimal(req::HTTP.Request)
    animalId = HTTP.URIs.splitpath(req.target)[5] # /api/zoo/v1/animals/10, get 10
    delete!(ANIMALS, animal.id)
    return ""
end

# add an additional endpoint for user creation
HTTP.@register(ANIMAL_ROUTER, "POST", "/api/zoo/v1/users", createUser)
# modify service endpoints to have user pass UUID in
HTTP.@register(ANIMAL_ROUTER, "GET", "/api/zoo/v1/users/*/animals/*", getAnimal)
HTTP.@register(ANIMAL_ROUTER, "DELETE", "/api/zoo/v1/users/*/animals/*", deleteAnimal)


HTTP.serve(CorsHandler, ip"127.0.0.1", 8080)

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, 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; body = 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", getItems)
HTTP.@register(ROUTER, "/api/events", HTTP.Handlers.StreamHandlerFunction(events))

HTTP.serve(ROUTER, "127.0.0.1", 8080)

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, JSON

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(HTTP.body(req)))
    square = body^2
    HTTP.Response(200, headers; body = string(square))
end

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

HTTP.serve(ROUTER, "127.0.0.1", 8080)

Readme Examples

#CLIENT

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

r = HTTP.request("GET", "http://httpbin.org/ip"; verbose=3)
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.serve() do request::HTTP.Request
   @show request
   @show request.method
   @show HTTP.header(request, "Content-Type")
   @show HTTP.payload(request)
   try
       return HTTP.Response("Hello")
   catch e
       return HTTP.Response(404, "Error: $e")
   end
end

#WebSocket Examples
@async HTTP.WebSockets.listen("127.0.0.1", UInt16(8081)) do ws
    while !eof(ws)
        data = readavailable(ws)
        write(ws, data)
    end
end

HTTP.WebSockets.open("ws://127.0.0.1:8081") do ws
    write(ws, "Hello")
    x = readavailable(ws)
    @show x
    println(String(x))
end;
x = UInt8[0x48, 0x65, 0x6c, 0x6c, 0x6f]
#Output: Hello

#=Custom HTTP Layer Examples
Notes:
There is no enforcement of a "well-defined" stack, you can insert a layer anywhere in the stack even if it logically does not make sense
When creating a custom layer, you need to create a request(), see below for an example
Custom layers is only implemented with the "low-level" request() calls, not the "convenience" functions such as HTTP.get(), HTTP.put(), etc.
module TestRequest=#
        import HTTP: Layer, request, Response

        abstract type TestLayer{Next <: Layer} <: Layer{Next} end
        export TestLayer, request

        function request(::Type{TestLayer{Next}}, io::IO, req, body; kw...)::Response where Next
                println("Insert your custom layer logic here!")
                return request(Next, io, req, body; kw...)
        end
end

using HTTP
using ..TestRequest

custom_stack = insert(stack(), StreamLayer, TestLayer)

result = request(custom_stack, "GET", "https://httpbin.org/ip")

# Insert your custom layer logic here!

# HTTP.Messages.Response:
# """
# HTTP/1.1 200 OK
# Access-Control-Allow-Credentials: true
# Access-Control-Allow-Origin: *
# Content-Type: application/json
# Date: Fri, 30 Aug 2019 14:13:17 GMT
# Referrer-Policy: no-referrer-when-downgrade
# Server: nginx
# X-Content-Type-Options: nosniff
# X-Frame-Options: DENY
# X-XSS-Protection: 1; mode=block
# Content-Length: 45
# Connection: keep-alive

# {
#   "origin": "--Redacted--"
# }
# """