HTTP.jl Documentation
Overview
HTTP.jl provides both client and server functionality for the http and websocket protocols. As a client, it provides the ability to make a wide range of requests, including GET, POST, websocket upgrades, form data, multipart, chunking, and cookie handling. There is also advanced functionality to provide client-side middleware and generate your own customized HTTP client. On the server side, it provides the ability to listen, accept, and route http requests, with middleware and handler interfaces to provide flexibility in processing responses.
Quickstart
Making requests (client)
HTTP.request
sends an http request and returns a response.
# make a GET request, both forms are equivalent
resp = HTTP.request("GET", "http://httpbin.org/ip")
resp = HTTP.get("http://httpbin.org/ip")
println(resp.status)
println(String(resp.body))
# make a POST request, sending data via `body` keyword argument
resp = HTTP.post("http://httpbin.org/body"; body="request body")
# make a POST request, sending form-urlencoded body
resp = HTTP.post("http://httpbin.org/body"; body=Dict("nm" => "val"))
# include query parameters in a request
# and turn on verbose logging of the request/response process
resp = HTTP.get("http://httpbin.org/anything"; query=["hello" => "world"], verbose=2)
# simple websocket client
WebSockets.open("ws://websocket.org") do ws
# we can iterate the websocket
# where each iteration yields a received message
# iteration finishes when the websocket is closed
for msg in ws
# do stuff with msg
# send back message as String, Vector{UInt8}, or iterable of either
send(ws, resp)
end
end
Handling requests (server)
HTTP.serve
allows specifying middleware + handlers for how incoming requests should be processed.
# authentication middleware to ensure property security
function auth(handler)
return function(req)
ident = parse_auth(req)
if ident === nothing
# failed to security authentication
return HTTP.Response(401, "unauthorized")
else
# store parsed identity in request context for handler usage
req.context[:auth] = ident
# pass request on to handler function for further processing
return handler(req)
end
end
end
# handler function to return specific user's data
function handler(req)
ident = req.context[:auth]
return HTTP.Response(200, get_user_data(ident))
end
# start a server listening on port 8081 (default port) for localhost (default host)
# requests will first be handled by teh auth middleware before being passed to the `handler`
# request handler function
HTTP.serve(auth(handler))
# websocket server is very similar to client usage
WebSockets.listen("0.0.0.0", 8080) do ws
for msg in ws
# simple echo server
send(ws, msg)
end
end
Further Documentation
Check out the client, server, and websocket-specific documentation pages for more in-depth discussions and examples for the many configurations available.
Migrating Legacy Code to 1.0
The 1.0 release is finally here! It's been a lot of work over the course of about 9 months combing through every part of the codebase to try and modernize APIs, fix long-standing issues, and bring the level of functionality up to par with other language http implementations. Along the way, some breaking changes were made, but with the aim that the package will now be committed to current published APIs for a long time to come. With the amount of increased functionality and fixes, we hope it provides enough incentive to make the update; as always, if you run into issues upgrading or feel something didn't get polished or fixed quite right, don't hesitate to open an issue so we can help.
The sections below outline a mix of breaking changes that were made, in addition to some of the new features in 1.0 with the aim to help those updating legacy codebases.
Struct Changes
- The
HTTP.Request
andHTTP.Response
body
fields are not restricted toVector{UInt8}
; if aresponse_stream
is passed toHTTP.request
, it will be set as theresp.body
(previously the body was an emptyUInt8[]
). This simplified many codepaths so these "other body object types" didn't have to be held in some other state, but could be stored in theRequest
/Response
directly. It also opens up the possibility, (as shown in the Cors Server example), where middleware can serialize/deserialize to/from thebody
field directly. - In related news, a
Request
body can now be passed as aDict
orNamedTuple
to have the key-value pairs serialized in theappliction/x-www-form-urlencoded
Content-Type matching many other libraries functionality - Responses with the
Transfer-Encoding: gzip
header will now also be automatically decompressed, and this behavior is configurable via thedecompress::Bool
keyword argument forHTTP.request
- If a
response_stream
is provided for streaming a request's response body,HTTP.request
will not callclose
before returning, leaving that up to the caller.
In addition, in the face of redirects or retried requests, note the response_stream
will not be written to until the final response is received.
- If a streaming request
body
is provided, it should support themark
/reset
methods in case the request needs to be retried. - Users are encouraged to access the publicly documented fields of
Request
/Response
instead of the previously documented "accessor" functions; these fields are now committed as the public API, so feel free to doresp.body
instead ofHTTP.body(resp)
. The accessor methods are still defined for backwards compat. - The
Request
object now stores the originalurl
argument provided toHTTP.request
as a parsedURIs.URI
object, and accessed via thereq.url
field. This is commonly desired in handlers/middleware, so convenient to keep it around. - The
Request
object also has a newreq.context
field of typeDict{Symbol, Any}
for storing/sharing state between handler/middleware layers. For example, theHTTP.Router
now parses and stores named path parameters with the:params
key in the context for handlers to access. AnotherHTTP.cookie_middleware
will parse and store any requestCookie
header in the:cookies
context key. HTTP.request
now throws more consistent and predictable error types, including (and restricted to):HTTP.ConnectError
,HTTP.StatusError
,HTTP.TimeoutError
, andHTTP.RequestError
. See the Request exceptions section for more details on each exception type.- Cookie persistence used to use a
Dict
per thread to store domain-specific cookie sessions. A new threadsafeCookieJar
struct now globally manages cookie persistence by default. Users can still construct and pass their owncookiejar
keyword argument toHTTP.request
if desired.
Keyword Argument Changes
- The
pipeline_limit
keyword argument (and support for it) were removed inHTTP.request
; the implementation was poor and it drastically complicated the request internal implementation. In addition, it's not commonly supported in other modern http implementations, which encourage use of HTTP/2 for better designed functionality. reuse_limit
support was removed in bothHTTP.request
andHTTP.listen
; another feature that complicated code more than it was actually useful and hence removed.aws_authentication
and its related keyword arguments have been removed in favor of using the AWS.jl package- A new
redirect_method
keyword argument exists and supports finer-grained control over which method to use in the case of a request redirect
Other Largish Changes
"Handlers" framework overhaul
The server-side Handlers framework has been changed to a more modern and flexible framework, including the Handler
and Middleware
interfaces. It's similar in ways to the old interfaces, but in our opinion, simpler and more straightforward with the clear distinction/pattern between what a Handler
does vs. a Middlware
.
In that vein, HTTP.Handlers.handle
has been removed. HTTP.serve
expects a single request or stream Handler
function, which should be of the form f(::Request)::Response
for the request case, or f(::Stream)::Nothing
for streams.
There are also plans to either include some common useful middleware functions in HTTP.jl directly, or a sister package specifically for collecting useful middlewares people can reuse.
WebSockets overhaul
The WebSockets code was some of the oldest and least maintained code in HTTP.jl. It was debated removing it entirely, but there aren't really other modern implementations that are well-maintained. So the WebSockets code was overhauled, modernized, and is now tested against the industry standard autobahn test suite (yay for 3rd party verification!). The API changed as well; while WebSockets.open
and WebSockets.listen
have stayed the same, the WebSocket
object itself now doesn't subtype IO
and has a restricted interface like:
ws.id
access a unique generated UUID id for this websocket connectionreceive(ws)
receive a single non-control message on a websocket, returning aString
orVector{UInt8}
depending on whether the message was sent as TEXT or BINARYsend(ws, msg)
send a message; supports TEXT and BINARY messages, and can provide an iterable formsg
to send fragmented messagesclose(ws)
close a websocket connection- For convenience, a
WebSocket
object can be iterated, where each iteration yields a non-control message and iteration terminates when the connection is closed
HTTP.Router reimplementation
While clever, the old HTTP.Router
implementation relied on having routes registered "statically", which can be really inconvenient for any cases where the routes are generated programmatically or need to be set/updated dynamically.
The new HTTP.Router
implementation uses a text-matching based trie data structure on incoming request path segments to find the right matching handler to process the request. It also supports parsing and storing path variables, like /api/{id}
or double wildcards for matching trailing path segments, like /api/**
.
HTTP.Router
now also supports complete unrestricted route registration via HTTP.register!
.
Internal client-side layers overhaul
While grandiose in vision, the old type-based "layers" framework relied heavily on type parameter abuse for generating a large "stack" of layers to handle different parts of each HTTP.request
. The new framework actually matches very closely with the server-side Handler
and Middleware
interfaces, and can be found in more detail under the Client-side Middleware (Layers) section of the docs. The new implementation, while hopefully bringing greater consistency between client-side and server-side frameworks, is much simpler and forced a large cleanup of state-handling in the HTTP.request
process for the better.
In addition to the changing of all the client-side layer definitions, HTTP.stack
now behaves slightly different in returning the new "layer" chain for HTTP.request
, while also accepting custom request/stream layers is provided. A new HTTP.@client
macro is provided for convenience in the case that users want to write a custom client-side middleware/layer and wrap its usage in an HTTP.jl-like client.
There also existed a few internal methods previously for manipulating the global stack of client-side layers (insert, insert_default!, etc.). These have been removed and replaced with a more formal (and documented) API via HTTP.pushlayer!
and HTTP.poplayer!
. These can be used to globally manipulate the client-side stack of layers for any HTTP.request
that is made.