Public Interface

Requests

HTTP.requestFunction
HTTP.request(method, url [, headers [, body]]; <keyword arguments>]) -> HTTP.Response

Send a HTTP Request Message and receive a HTTP Response Message.

e.g.

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

headers can be any collection where [string(k) => string(v) for (k,v) in headers] yields Vector{Pair}. e.g. a Dict(), a Vector{Tuple}, a Vector{Pair} or an iterator.

body can take a number of forms:

  • a String, a Vector{UInt8} or any T accepted by write(::IO, ::T)
  • a collection of String or AbstractVector{UInt8} or IO streams or items of any type T accepted by write(::IO, ::T...)
  • a readable IO stream or any IO-like type T for which eof(T) and readavailable(T) are defined.

The HTTP.Response struct contains:

  • status::Int16 e.g. 200
  • headers::Vector{Pair{String,String}} e.g. ["Server" => "Apache", "Content-Type" => "text/html"]
  • body::Vector{UInt8}, the Response Body bytes (empty if a response_stream was specified in the request).

Functions HTTP.get, HTTP.put, HTTP.post and HTTP.head are defined as shorthand for HTTP.request("GET", ...), etc.

HTTP.request and HTTP.open also accept optional keyword parameters.

e.g.

HTTP.request("GET", "http://httpbin.org/ip"; retries=4, cookies=true)

HTTP.get("http://s3.us-east-1.amazonaws.com/"; aws_authorization=true)

conf = (readtimeout = 10,
        pipeline_limit = 4,
        retry = false,
        redirect = false)

HTTP.get("http://httpbin.org/ip"; conf...)
HTTP.put("http://httpbin.org/put", [], "Hello"; conf...)

URL options

  • query = nothing, replaces the query part of url.

Streaming options

  • response_stream = nothing, a writeable IO stream or any IO-like type T for which write(T, AbstractVector{UInt8}) is defined.
  • verbose = 0, set to 1 or 2 for extra message logging.

Connection Pool options

  • connect_timeout = 0, close the connection after this many seconds if it is still attempting to connect. Use connect_timeout = 0 to disable.
  • connection_limit = 8, number of concurrent connections to each host:port.
  • pipeline_limit = 16, number of concurrent requests per connection.
  • reuse_limit = nolimit, number of times a connection is reused after the first request.
  • socket_type = TCPSocket

Timeout options

  • readtimeout = 0, close the connection if no data is received for this many seconds. Use readtimeout = 0 to disable.

Retry options

  • retry = true, retry idempotent requests in case of error.
  • retries = 4, number of times to retry.
  • retry_non_idempotent = false, retry non-idempotent requests too. e.g. POST.

Redirect options

  • redirect = true, follow 3xx redirect responses.
  • redirect_limit = 3, number of times to redirect.
  • forwardheaders = true, forward original headers on redirect.

Status Exception options

  • status_exception = true, throw HTTP.StatusError for response status >= 300.

SSLContext options

Basic Authentication options

  • Basic authentication is detected automatically from the provided url's userinfo (in the form scheme://user:password@host) and adds the Authorization: Basic header

AWS Authentication options

  • aws_authorization = false, enable AWS4 Authentication.
  • aws_service = split(url.host, ".")[1]
  • aws_region = split(url.host, ".")[2]
  • aws_access_key_id = ENV["AWS_ACCESS_KEY_ID"]
  • aws_secret_access_key = ENV["AWS_SECRET_ACCESS_KEY"]
  • aws_session_token = get(ENV, "AWS_SESSION_TOKEN", "")
  • body_sha256 = digest(MD_SHA256, body),
  • body_md5 = digest(MD_MD5, body),

Cookie options

  • cookies::Union{Bool, Dict{<:AbstractString, <:AbstractString}} = false, enable cookies, or alternatively, pass a Dict{AbstractString, AbstractString} of name-value pairs to manually pass cookies
  • cookiejar::Dict{String, Set{Cookie}}=default_cookiejar,

Canonicalization options

  • canonicalize_headers = false, rewrite request and response headers in Canonical-Camel-Dash-Format.

Proxy options

  • proxy = proxyurl, pass request through a proxy given as a url

Alternatively, HTTP.jl also respects the http_proxy, HTTP_PROXY, https_proxy, HTTPS_PROXY, and no_proxy environment variables; if set, they will be used automatically when making requests.

Request Body Examples

String body:

HTTP.request("POST", "http://httpbin.org/post", [], "post body data")

Stream body from file:

io = open("post_data.txt", "r")
HTTP.request("POST", "http://httpbin.org/post", [], io)

Generator body:

chunks = ("chunk$i" for i in 1:1000)
HTTP.request("POST", "http://httpbin.org/post", [], chunks)

Collection body:

chunks = [preamble_chunk, data_chunk, checksum(data_chunk)]
HTTP.request("POST", "http://httpbin.org/post", [], chunks)

open() do io body:

HTTP.open("POST", "http://httpbin.org/post") do io
    write(io, preamble_chunk)
    write(io, data_chunk)
    write(io, checksum(data_chunk))
end

Response Body Examples

String body:

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

Stream body to file:

io = open("get_data.txt", "w")
r = HTTP.request("GET", "http://httpbin.org/get", response_stream=io)
close(io)
println(read("get_data.txt"))

Stream body through buffer:

io = Base.BufferStream()
@async while !eof(io)
    bytes = readavailable(io)
    println("GET data: $bytes")
end
r = HTTP.request("GET", "http://httpbin.org/get", response_stream=io)
close(io)

Stream body through open() do io:

r = HTTP.open("GET", "http://httpbin.org/stream/10") do io
   while !eof(io)
       println(String(readavailable(io)))
   end
end

using HTTP.IOExtras

HTTP.open("GET", "https://tinyurl.com/bach-cello-suite-1-ogg") do http
    n = 0
    r = startread(http)
    l = parse(Int, HTTP.header(r, "Content-Length"))
    open(`vlc -q --play-and-exit --intf dummy -`, "w") do vlc
        while !eof(http)
            bytes = readavailable(http)
            write(vlc, bytes)
            n += length(bytes)
            println("streamed $n-bytes $((100*n)÷l)%\u1b[1A")
        end
    end
end

Request and Response Body Examples

String bodies:

r = HTTP.request("POST", "http://httpbin.org/post", [], "post body data")
println(String(r.body))

Interfacing with RESTful JSON APIs:

using JSON
params = Dict("user"=>"RAO...tjN", "token"=>"NzU...Wnp", "message"=>"Hello!")
base_url = "http://api.domain.com"
endpoint = "/1/messages.json"
url = base_url * endpoint
r = HTTP.request("POST", url,
             ["Content-Type" => "application/json"],
             JSON.json(params))
println(JSON.parse(String(r.body)))

Stream bodies from and to files:

in = open("foo.png", "r")
out = open("foo.jpg", "w")
HTTP.request("POST", "http://convert.com/png2jpg", [], in, response_stream=out)

Stream bodies through: open() do io:

using HTTP.IOExtras

HTTP.open("POST", "http://music.com/play") do io
    write(io, JSON.json([
        "auth" => "12345XXXX",
        "song_id" => 7,
    ]))
    r = startread(io)
    @show r.status
    while !eof(io)
        bytes = readavailable(io)
        play_audio(bytes)
    end
end
source
HTTP.openFunction
HTTP.open(method, url, [,headers]) do io
    write(io, body)
    [startread(io) -> HTTP.Response]
    while !eof(io)
        readavailable(io) -> AbstractVector{UInt8}
    end
end -> HTTP.Response

The HTTP.open API allows the Request Body to be written to (and/or the Response Body to be read from) an IO stream.

e.g. Streaming an audio file to the vlc player:

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
source
HTTP.getFunction
HTTP.get(url [, headers]; <keyword arguments>) -> HTTP.Response

Shorthand for HTTP.request("GET", ...). See HTTP.request.

source
HTTP.putFunction
HTTP.put(url, headers, body; <keyword arguments>) -> HTTP.Response

Shorthand for HTTP.request("PUT", ...). See HTTP.request.

source
HTTP.postFunction
HTTP.post(url, headers, body; <keyword arguments>) -> HTTP.Response

Shorthand for HTTP.request("POST", ...). See HTTP.request.

source
HTTP.headFunction
HTTP.head(url; <keyword arguments>) -> HTTP.Response

Shorthand for HTTP.request("HEAD", ...). See HTTP.request.

source

Request body types

HTTP.FormType
Form(data; boundary=string(rand(UInt128), base=16))

Construct a request body for multipart/form-data encoding from data.

data must iterate key-value pairs (e.g. Dict or Vector{Pair}) where the key/value of the iterator is the key/value of each mutipart boundary chunk. Files and other large data arguments can be provided as values as IO arguments: either an IOStream such as returned via open(file), or an IOBuffer for in-memory data.

For complete control over a multipart chunk's details, an HTTP.Multipart type is provided to support setting the filename, Content-Type, and Content-Transfer-Encoding.

Examples

data = Dict(
    "text" => "text data",
    # filename (cat.png) and content-type (image/png) inferred from the IOStream
    "file1" => open("cat.png"),
    # manully controlled chunk
    "file2" => HTTP.Multipart("dog.jpeg", open("mydog.jpg"), "image/jpeg"),
)
body = HTTP.Form(data)
headers = []
HTTP.post(url, headers, body)
source
HTTP.MultipartType
Multipart(filename::String, data::IO, content_type=HTTP.sniff(data), content_transfer_encoding="")

A type to represent a single multipart upload chunk for a file. This type would be used as the value in a key-value pair when constructing a HTTP.Form for a request body (see example below). The data argument must be an IO type such as IOStream, or IOBuffer. The content_type and content_transfer_encoding arguments allow manual setting of these multipart headers. Content-Type will default to the result of the HTTP.sniff(data) mimetype detection algorithm, whereas Content-Transfer-Encoding will be left out if not specified.

Examples

body = HTTP.Form(Dict(
    "key" => HTTP.Multipart("File.txt", open("MyFile.txt"), "text/plain"),
))
headers = []
HTTP.post(url, headers, body)

Extended help

Filename SHOULD be included when the Multipart represents the contents of a file RFC7578 4.2

Content-Disposition set to "form-data" MUST be included with each Multipart. An additional "name" parameter MUST be included An optional "filename" parameter SHOULD be included if the contents of a file are sent This will be formatted such as: Content-Disposition: form-data; name="user"; filename="myfile.txt" RFC7578 4.2

Content-Type for each Multipart is optional, but SHOULD be included if the contents of a file are sent. RFC7578 4.4

Content-Transfer-Encoding for each Multipart is deprecated RFC7578 4.7

Other Content- header fields MUST be ignored RFC7578 4.8

source

Request exceptions

Request functions may throw the following exceptions:

Sockets.DNSErrorType
DNSError

The type of exception thrown when an error occurs in DNS lookup. The host field indicates the host URL string. The code field indicates the error code based on libuv.

URIs

HTTP.jl uses the URIs.jl package for handling URIs. Some functionality from URIs.jl, relevant to HTTP.jl, are listed below:

URIs.URIType
URI(; scheme="", host="", port="", etc...)
URI(str) = parse(URI, str::String)

A type representing a URI (e.g. a URL). Can be constructed from distinct parts using the various supported keyword arguments, or from a string. The URI constructors will automatically escape any provided query arguments, typically provided as "key"=>"value"::Pair or Dict("key"=>"value"). Note that multiple values for a single query key can provided like Dict("key"=>["value1", "value2"]).

When constructing a URI from a String, you need to first unescape that string: URI( URIs.unescapeuri(str) ).

The URI struct stores the complete URI in the uri::String field and the component parts in the following SubString fields:

  • scheme, e.g. "http" or "https"
  • userinfo, e.g. "username:password"
  • host e.g. "julialang.org"
  • port e.g. "80" or ""
  • path e.g "/"
  • query e.g. "Foo=1&Bar=2"
  • fragment

The queryparams(::URI) function returns a Dict containing the query.

URIs.escapeuriFunction
escapeuri(x)

Apply URI percent-encoding to escape special characters in x.

URIs.unescapeuriFunction
unescapeuri(str)

Percent-decode a string according to the URI escaping rules.

URIs.splitpathFunction
URIs.splitpath(path|uri; rstrip_empty_segment=true)

Splits the path into component segments based on /, according to http://tools.ietf.org/html/rfc3986#section-3.3. Any fragment and query parts of the string are ignored if present.

A final empty path segment (trailing '/') is removed, if present. This is technically incompatible with the segment grammar of RFC3986, but it seems to be a common recommendation to make paths with and without a trailing slash equivalent. To preserve any final empty path segment, set rstrip_empty_segment=false.

Examples

julia> URIs.splitpath(URI("http://example.com/foo/bar?a=b&c=d"))
2-element Array{String,1}:
 "foo"
 "bar"

julia> URIs.splitpath("/foo/bar/")
2-element Array{String,1}:
 "foo"
 "bar"

Cookies

HTTP.Cookies.CookieType
Cookie()
Cookie(; kwargs...)
Cookie(name, value; kwargs...)

A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an HTTP response or the Cookie header of an HTTP request. Supported fields (which can be set using keyword arguments) include:

  • name: name of the cookie
  • value: value of the cookie
  • path: applicable path for the cookie
  • domain: applicable domain for the cookie
  • expires: a Dates.DateTime representing when the cookie should expire
  • maxage: maxage == 0 means no max age, maxage < 0 means delete cookie now, max age > 0 means the # of seconds until expiration
  • secure::Bool: secure cookie attribute
  • httponly::Bool: httponly cookie attribute
  • hostonly::Bool: hostonly cookie attribute

See http:#tools.ietf.org/html/rfc6265 for details.

source

Utilities

HTTP.sniffFunction

HTTP.sniff(content::Union{Vector{UInt8}, String, IO}) => String (mimetype)

HTTP.sniff will look at the first 512 bytes of content to try and determine a valid mimetype. If a mimetype can't be determined appropriately, "application/octet-stream" is returned.

Supports JSON detection through the HTTP.isjson(content) function.

Examples

julia> HTTP.sniff("Hello world!!")
"text/plain; charset=utf-8"

julia> HTTP.sniff("<html><body>Hello world!!</body></html>")
"text/html; charset=utf-8"

julia> HTTP.sniff("{"a": -1.0}")
"application/json; charset=utf-8"
source
HTTP.Messages.statustextFunction
statustext(::Int) -> String

String representation of a HTTP status code.

Examples

julia> statustext(200)
"OK"

julia> statustext(404)
"Not Found"
source

Server / Handlers

HTTP.Servers.listenFunction
HTTP.listen([host=Sockets.localhost[, port=8081]]; kw...) do http::HTTP.Stream
    ...
end

Listen for HTTP connections and execute the do function for each request.

The do function should be of the form f(::HTTP.Stream)::Nothing, and should at the minimum set a status via setstatus() and call startwrite() either explicitly or implicitly by writing out a response via write(). Failure to do this will result in an HTTP 500 error being transmitted to the client.

Optional keyword arguments:

  • sslconfig=nothing, Provide an MbedTLS.SSLConfig object to handle ssl connections. Pass sslconfig=MbedTLS.SSLConfig(false) to disable ssl verification (useful for testing).
  • reuse_limit = nolimit, number of times a connection is allowed to be reused after the first request.
  • tcpisvalid = tcp->true, function f(::TCPSocket)::Bool to, check accepted connection before processing requests. e.g. to do source IP filtering.
  • readtimeout::Int=0, close the connection if no data is received for this many seconds. Use readtimeout = 0 to disable.
  • reuseaddr::Bool=false, allow multiple servers to listen on the same port.
  • server::Base.IOServer=nothing, provide an IOServer object to listen on; allows closing the server.
  • connection_count::Ref{Int}, reference to track the number of currently open connections.
  • rate_limit::Rational{Int}=nothing", number of connections//second allowed per client IP address; excess connections are immediately closed. e.g. 5//1.
  • verbose::Bool=false, log connection information to stdout.
  • access_log::Function, function for formatting access log messages. The function should accept two arguments, io::IO to which the messages should be written, and http::HTTP.Stream which can be used to query information from. See also @logfmt_str.
  • on_shutdown::Union{Function, Vector{<:Function}, Nothing}=nothing, one or more functions to be run if the server is closed (for example by an InterruptException). Note, shutdown function(s) will not run if an IOServer object is supplied to the server keyword argument and closed by close(server).

e.g.

HTTP.listen("127.0.0.1", 8081) do http
    HTTP.setheader(http, "Content-Type" => "text/html")
    write(http, "target uri: $(http.message.target)<BR>")
    write(http, "request body:<BR><PRE>")
    write(http, read(http))
    write(http, "</PRE>")
    return
end

HTTP.listen("127.0.0.1", 8081) do http
    @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")
    startwrite(http)
    write(http, "response body")
    write(http, "more response body")
end

The server= option can be used to pass an already listening socket to HTTP.listen. This allows manual control of server shutdown.

e.g.

using Sockets
server = Sockets.listen(Sockets.InetAddr(parse(IPAddr, host), port))
@async HTTP.listen(f, host, port; server=server)

# Closing server will stop HTTP.listen.
close(server)

To run the following HTTP chat example, open two Julia REPL windows and paste the example code into both of them. Then in one window run chat_server() and in the other run chat_client(), then type hello and press return. Whatever you type on the client will be displayed on the server and vis-versa.

using HTTP

function chat(io::HTTP.Stream)
    @async while !eof(io)
        write(stdout, readavailable(io), "\n")
    end
    while isopen(io)
        write(io, readline(stdin))
    end
end

chat_server() = HTTP.listen("127.0.0.1", 8087) do io
    write(io, "HTTP.jl Chat Server. Welcome!")
    chat(io)
end

chat_client() = HTTP.open("POST", "http://127.0.0.1:8087") do io
    chat(io)
end
source
HTTP.Handlers.serveFunction
HTTP.serve([host=Sockets.localhost[, port=8081]]; kw...) do req::HTTP.Request
    ...
end
HTTP.serve([host=Sockets.localhost[, port=8081]]; stream=true, kw...) do stream::HTTP.Stream
    ...
end
HTTP.serve(handler, [host=Sockets.localhost[, port=8081]]; kw...)

Listen for HTTP connections and handle each request received. The "handler" can be a function that operates directly on HTTP.Stream, HTTP.Request, or any kind of HTTP.Handler instance. For functions like f(::HTTP.Stream), also pass stream=true to signal a streaming handler.

Optional keyword arguments:

  • sslconfig=nothing, Provide an MbedTLS.SSLConfig object to handle ssl connections. Pass sslconfig=MbedTLS.SSLConfig(false) to disable ssl verification (useful for testing)
  • reuse_limit = nolimit, number of times a connection is allowed to be reused after the first request.
  • tcpisvalid::Function (::TCPSocket) -> Bool, check accepted connection before processing requests. e.g. to implement source IP filtering, rate-limiting, etc.
  • readtimeout::Int=0, close the connection if no data is received for this many seconds. Use readtimeout = 0 to disable.
  • reuseaddr::Bool=false, allow multiple server processes to listen on the same port. Only fully supported on linux; OSX will allow multiple server processes to listen, but only one will accept connections
  • server::Base.IOServer=nothing, provide an IOServer object to listen on; allows manual control over closing the server.
  • connection_count::Ref{Int}, reference to track the # of currently open connections.
  • rate_limit::Rational{Int}=nothing", number of connections//second allowed per client IP address; excess connections are immediately closed. e.g. 5//1.
  • stream::Bool=false, the handler will operate on an HTTP.Stream instead of HTTP.Request
  • verbose::Bool=false, log connection information to stdout.
  • on_shutdown::Union{Function, Vector{<:Function}, Nothing}=nothing, one or more functions to be run if the server is closed (for example by an InterruptException). Note, shutdown function(s) will not run if an IOServer object is supplied to the server keyword argument and closed by close(server).

Examples

HTTP.serve(; stream=true) 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")
    startwrite(http)
    write(http, "response body")
    write(http, "more response body")
    return
end

# pass in own server socket to control shutdown
using Sockets
server = Sockets.listen(Sockets.InetAddr(parse(IPAddr, host), port))
@async HTTP.serve(f, host, port; server=server)
# close server which will stop HTTP.serve
close(server)
source
HTTP.HandlersModule

Examples

Let's put together an example http REST server for our hypothetical "ZooApplication" that utilizes various parts of the Servers & Handler frameworks.

Our application allows users to interact with custom "animal" JSON objects.

First we have our "model" or data structures:

mutable struct Animal
    id::Int
    type::String
    name::String
end

Now we want to define our REST api, or how do we allow users to create, update, retrieve and delete animals:

# 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)

Great! At this point, we could spin up our server and let users start managing their animals:

HTTP.serve(ANIMAL_ROUTER, Sockets.localhost, 8081)

Now, you may have noticed that there was a bit of repetition in our "service" functions, particularly with regards to the JSON serialization/deserialization. Perhaps we can simplify things by writing a custom "JSONHandler" to do some of the repetitive work for us.

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

# **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

And we modify slightly how we run our server, letting our new JSONHandler be the entry point instead of our router:

HTTP.serve(JSONHandler, Sockets.localhost, 8081)

Our JSONHandler is nice because it saves us a bunch of repetition: if a request body comes in, we automatically deserialize it and pass it on to the service function. And each service function doesn't need to worry about returning HTTP.Responses anymore, but can just focus on returning plain Julia objects/strings. The other huge advantage is it provides a clean separation of concerns between the "service" layer, which should really concern itself with application logic, and the "REST API" layer, which should take care of translating between our model and a web data format (JSON).

Let's take this one step further and allow multiple users to manage users, and add in one more custom handler to provide an authentication layer to our application. We can't just let anybody be modifying another user's animals!

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

# modify our data store to allow for multiple users
const ANIMALS = Dict{Base.UUID, Dict{Int, Animal}}()

# creating a user returns a new UUID key unique to the user
createUser(req) = Base.UUID(rand(UInt128))

# 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)

# modified service functions to account for multiple users
function createAnimal(req::HTTP.Request, animal)
    animal.id = getNextId()
    ANIMALS[animal.userId][animal.id] = animal
    return animal
end

function getAnimal(req::HTTP.Request)
    paths = HTTP.URIs.splitpath(req.target)
    userId = path[5] # /api/zoo/v1/users/x92jf-.../animals/10, get user UUID
    animalId = path[7] # /api/zoo/v1/users/x92jf-.../animals/10, get 10
    return ANIMALS[userId][parse(Int, animalId)]
end

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

function deleteAnimal(req::HTTP.Request)
    paths = HTTP.URIs.splitpath(req.target)
    userId = path[5] # /api/zoo/v1/users/x92jf-.../animals/10, get user UUID
    animalId = path[7] # /api/zoo/v1/users/x92jf-.../animals/10, get 10
    delete!(ANIMALS[userId], parse(Int, animal.id))
    return ""
end

# AuthHandler to reject any unknown users
function AuthHandler(req)
    if HTTP.hasheader(req, "Animal-UUID")
        uuid = HTTP.header(req, "Animal-UUID")
        if haskey(ANIMALS, uuid)
            return JSONHandler(req)
        end
    end
    return HTTP.Response(401, "unauthorized")
end

And our modified server invocation:

HTTP.serve(AuthHandler, Sockets.localhost, 8081)

Let's review what's going on here:

  • Each Animal object now includes a UUID object unique to a user
  • We added a /api/zoo/v1/users endpoint for creating a new user
  • Each of our service functions now account for individual users
  • We made a new AuthHandler as the very first entry point in our middleware stack, this means that every single request must first pass through this authentication layer before reaching the service layer. Our AuthHandler checks that the user provided our security request header Animal-UUID and if so, ensures the provided UUID corresponds to a valid user. If not, the AuthHandler returns a 401 HTTP response, signalling that the request is unauthorized

Voila, hopefully that helps provide a slightly-more-than-trivial example of utilizing the HTTP.Handler framework in conjunction with running an HTTP server.

source
HTTP.Handlers.handleFunction
HTTP.handle(handler::HTTP.RequestHandler, ::HTTP.Request) => HTTP.Response
HTTP.handle(handler::HTTP.StreamHandler, ::HTTP.Stream)

Dispatch function used to handle incoming requests to a server. Can be overloaded by custom HTTP.Handler subtypes to implement custom "handling" behavior.

source
HTTP.Handlers.RequestHandlerFunctionType
RequestHandlerFunction(f)

A function-wrapper type that is a subtype of RequestHandler. Takes a single function as an argument that should be of the form f(::HTTP.Request) => HTTP.Response

source
HTTP.Handlers.StreamHandlerFunctionType
StreamHandlerFunction(f)

A function-wrapper type that is a subtype of StreamHandler. Takes a single function as an argument that should be of the form f(::HTTP.Stream) => Nothing, i.e. it accepts a raw HTTP.Stream, handles the incoming request, writes a response back out to the stream directly, then returns.

source
HTTP.Handlers.RouterType
HTTP.Router(h::Handler)
HTTP.Router(f::Function)
HTTP.Router()

An HTTP.Handler type that supports pattern matching request url paths to registered HTTP.Handlers. Can accept a default Handler or Function that will be used in case no other handlers match; by default, a 404 response handler is used. Paths can be mapped to a handler via HTTP.@register(r::Router, path, handler), see ?HTTP.@register for more details.

source
HTTP.Handlers.@registerMacro
HTTP.@register(r::Router, path, handler)
HTTP.@register(r::Router, method::String, path, handler)
HTTP.@register(r::Router, method::String, scheme::String, host::String, path, handler)

Function to map request urls matching path and optional method, scheme, host to another handler::HTTP.Handler. URL paths are registered one at a time, and multiple urls can map to the same handler. The URL can be passed as a String. Requests can be routed based on: method, scheme, hostname, or path. The following examples show how various urls will direct how a request is routed by a server:

  • "http://*": match all HTTP requests, regardless of path
  • "https://*": match all HTTPS requests, regardless of path
  • "/gmail": regardless of scheme or host, match any request with a path starting with "gmail"
  • "/gmail/userId/*/inbox: match any request matching the path pattern, "*" is used as a wildcard that matches any value between the two "/"

Note that due to being a macro (and the internal routing functionality), routes can only be registered statically, i.e. at the top level of a module, and not dynamically, i.e. inside a function.

source
HTTP.@logfmt_strMacro
logfmt"..."

Parse an NGINX-style log format string and return a function mapping (io::IO, http::HTTP.Stream) -> body suitable for passing to HTTP.listen using the access_log keyword argument.

The following variables are currently supported:

  • $http_name: arbitrary request header (with - replaced with _, e.g. http_user_agent)
  • $sent_http_name: arbitrary response header (with - replaced with _)
  • $request: the request line, e.g. GET /index.html HTTP/1.1
  • $request_method: the request method
  • $request_uri: the request URI
  • $remote_addr: client address
  • $remote_port: client port
  • $remote_user: user name supplied with the Basic authentication
  • $server_protocol: server protocol
  • $time_iso8601: local time in ISO8601 format
  • $time_local: local time in Common Log Format
  • $status: response status code
  • $body_bytes_sent: number of bytes in response body

Examples

logfmt"[$time_iso8601] \"$request\" $status" # [2021-05-01T12:34:40+0100] "GET /index.html HTTP/1.1" 200

logfmt"$remote_addr \"$http_user_agent\"" # 127.0.0.1 "curl/7.47.0"
source

Messages Interface

HTTP.Messages.RequestType
Request <: Message

Represents a HTTP Request Message.

  • method::String RFC7230 3.1.1

  • target::String RFC7230 5.3

  • version::VersionNumber RFC7230 2.6

  • headers::Vector{Pair{String,String}} RFC7230 3.2

  • body::Vector{UInt8} RFC7230 3.3

  • response, the Response to this Request

  • txcount, number of times this Request has been sent (see RetryRequest.jl).

  • parent, the Response (if any) that led to this request (e.g. in the case of a redirect). RFC7230 6.4

You can get each data with HTTP.method, HTTP.headers, HTTP.uri, and HTTP.body.

source