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)