Working with the HTTP Protocol and Forms in Julia
Using the HTTP, URIs, Sockets, JSON and FileIO packages
The Julia HTTP package is great for using the HTTP protocol to either make simple web servers, connect to web servers or use deal with REST APIs. However the package is not that well documented. Many things in the documentation such as best practices for testing REST APIs is not made clear.
Here I will cover:
- Mocking requests. Useful for testing your web service.
- Creating headers for your requests.
- How routing works — How a URL gets tied to a particular handler.
- Dealing with the payload. E.g. how to put images into the body of a message.
- Working with IP addresses and port numbers.
- Parsing the URI of a request.
- Sending data from user using HTML forms
Overview
There are a lot of different parts which come together and it is easy to miss the big picture if you are looking at the example code provided with HTTP.jl.
The HTTP requests a client sends to an HTTP server is represented by the HTTP.Request
object, while responses sent from server are represented by the HTTP.Response
type.
The HTTP.handle
function binds request and response objects together:
HTTP.handle(hand::HTTP.Handler, req::HTTP.Request) -> HTTP.Request
You can use different types of handlers, but normally we use the HTTP.Router
subtype, as this can route a request to different handling functions.
So let us summarize:
- HTTP.Requeset — Sent to
HTTP.handle
function which passes it to a handler. - HTTP.Reponse — Object returned from
handle
function, or more specifically from the various handler function registered with the router. - HTTP.Router — Used with the
HTTP.handle
function to route a request to handler function with signaturef(req::HTTP.Request)
.
This helps convey how the request and response objects are related. They are both of type HTTP.Message
.
Constructing Requests
Normally you don’t manually construct request objects, because you also want to send a request to a remote server.
julia> resp = HTTP.request("GET", "http://httpbin.org/ip")
HTTP.Messages.Response:
"""
HTTP/1.1 200 OK
Content-Type: application/json
{
"origin": "62.16.239.222"
}
"""
Not that the HTTP.Reponse
object returned is given as HTTP.Messages.Response
. That is because all request and response objects are defined in the Messages
submodule. HTTP.Reponse
is simply an alias for HTTP.Messages.Response
.
As you can see this sends and actual request over the network. But what if you want to test your handler function instead? You could spin up a server with this code:
using HTTP
using Sockets # Provides IPv4 type and IP number literal.
# Define handler functions
foo(req::HTTP.Request) = HTTP.Response(200, "Called foo")
bar(req::HTTP.Request) = HTTP.Response(200, "Called bar")
# Setup routing to handler functions
const router = HTTP.Router()
HTTP.@register(router, "GET", "/foo", foo)
HTTP.@register(router, "GET", "/bar", bar)
# Setup asyncronous task to perform listening to requests.
# Avoid blocking REPL
HTTP.serve(router, ip"127.0.0.1", 8080)
You could test this by using a web browser and go to address http://localhost:8080/bar
or we can make a request from Julia. You will have to open another terminal window or tab as HTTP.serve
will block.
julia> resp = HTTP.request("GET", "http://localhost:8080/bar")
HTTP.Messages.Response:
"""
HTTP/1.1 200 OK
Transfer-Encoding: chunked
Called bar"""
Mocking Requests
If you don’t want to spin up a server, we can mock the requests instead. So we make a request object without actually sending it anywhere.
julia> req = HTTP.Request(
"GET", # Could be GET, POST, UPDATE etc
"http://localhost:8080/foo", # URL
["Content-Type" => "text/plain"], # Header fields
"hello" # Payload/body
)
HTTP.Messages.Request:
"""
GET http://localhost HTTP/1.1
Content-Type: text/plain
hello"""
By calling HTTP.handle
directly we route to the handler functions without actually running a server looking for network communications:
julia> resp = HTTP.handle(router, req)
HTTP.Messages.Response:
"""
HTTP/1.1 200 OK
Called foo"""
Access Content in Requests and Responses
When we get a request in our handler or receive a response, there is various data of interest which we would like to access. E.g. what are the headers, the payload, the URI etc.
For this we use the HTTP.method
, HTTP.headers
, HTTP.uri
and HTTP.payload
functions.
HTTP.Request
Let us make a HTTP.Request
object to demonstrate:
julia> req = HTTP.Request(
"GET",
"http://foo/bar?qux=123",
["Content-Type" => "text/html"],
"hello")
HTTP.Messages.Request:
"""
GET http://foo/bar?qux=123 HTTP/1.1
Content-Type: text/html
hello"""
Let us retrieve the different parts again (There are methods for this but they have been deprecated so assume direct field access is preferred).
julia> req.method
"GET"
julia> reg.target
"http://foo/bar?qux=123"
julia> reg.headers
1-element Vector{Pair{SubString{String}, SubString{String}}}:
"Content-Type" => "text/html"
However this may not be practical in dealing with headers, so there are higher level functions to make it easier:
julia> req["Content-Type"]
"text/html"
julia> HTTP.header(req, "Content-Type")
"text/html"
The payload is stored as bytes, and must be converted to a string:
julia> HTTP.payload(req)
5-element Vector{UInt8}:
0x68
0x65
0x6c
0x6c
0x6f
julia> String(HTTP.payload(req))
"hello"
HTTP.Response
Accessing response data uses similar methods. One thing to note is that functions to create response objects don’t follow exactly the same pattern as for HTTP.Request
. The body
or payload is a named argument.
julia> resp = HTTP.Response(
200, # status code, meaning success.
["Content-Type" => "text/plain"],
body = "hello")
HTTP.Messages.Response:
"""
HTTP/1.1 200 OK
Content-Type: text/plain
hello"""
Apropos: List of HTTP status codes.
Working with URIs
This can be a bit confusing if you are new to Julia. A lot of functionality is often split over several libraries. HTTP
itself does not handle URIs but the URIs package.
julia> using URIs
julia> uri = URI(HTTP.uri(req))
URI("http://foo/bar?qux=123")
julia> uri.scheme
"http"
julia> uri.host
"foo"
julia> uri.path
"/bar"
julia> uri.query
"qux=123"
julia> queryparams(uri)
Dict{String, String} with 1 entry:
"qux" => "123"
IP Address and Port Numbers
To work with IP addresses we need the use the Sockets library. Again not functionality found in the HTTP library.
We can do this in a variety of ways. One way is to use the parse
function which deals with many types in Julia. E.g. you can use it to parse a string and create an Integer, Boolean etc.
julia> parse(Int, "42")
42
julia> parse(Bool, "true")
true
julia> parse(Float64, "42")
42.0
This regularity or consistency in Julia is worth being aware when you contemplate doing something with a new library. Sockets
provide the IPv4
type and it turns out you can parse to this one as well:
julia> using Sockets
julia> parse(IPAddr, "127.0.0.1")
ip"127.0.0.1"
The result of the parsing may give you a hint at an alternative way of creating these IP addresses. That is to use the ip""
style string literals.
julia> addr = ip"127.0.0.1"
ip"127.0.0.1"
julia> typeof(addr)
IPv4
But why prefer this solution? String literals are macros. So ip""
is actually the @ip_str
macro. And macros are evaluated at parse time, not runtime. That means if you put parse(IPAddr, "127.0.0.1")
inside a function, it will get parsed every time the function is called.ip"127.0.0.1"
will only get parsed once in contrast.
Working with JSON Data
Let us look at how we push and pull JSON data out of requests and responses. First we use JSON.json(dict)
to convert a Julia dictionary dict
to a JSON
string and stuff it in a HTTP.Response
.
julia> using JSON
julia> size = Dict(
"width" => 20,
"height" => 30);
julia> resp = HTTP.Response(
200,
["Content-Type" => "application/json"],
body=JSON.json(size)
)
HTTP.Messages.Response:
"""
HTTP/1.1 200 OK
Content-Type: application/json
{"height":30,"width":20}"""
Now imagine the server sent this response and the client has to pull out the JSON data. How would we do that?
julia> body = HTTP.payload(resp)
24-element Vector{UInt8}:
0x7b
0x22
⋮
0x32
0x30
0x7d
julia> io = IOBuffer(body);
julia> JSON.parse(io)
Dict{String, Any} with 2 entries:
"height" => 30
"width" => 20
Although you don’t need to supply an IO
object to JSON.parse
, you can also just give a JSON string.
julia> JSON.parse("{\"height\":30,\"width\":20}")
Dict{String, Any} with 2 entries:
"height" => 30
"width" => 20
You got to be aware of some gotchas however, which is that String
takes ownership of the input byte data, so it is gone after you turn the bytes into a string.
julia> s = String(HTTP.payload(resp))
"{\"height\":30,\"width\":20}"
julia> s = String(HTTP.payload(resp))
""
To avoid this you would have to use copy
:
julia> String(copy(HTTP.payload(resp)))
"{\"height\":30,\"width\":20}"
julia> String(copy(HTTP.payload(resp)))
"{\"height\":30,\"width\":20}"
julia> String(copy(HTTP.payload(resp)))
"{\"height\":30,\"width\":20}"
Working With Images
How do you put an image into an HTTP request? That is what we will cover here.
Putting an image into a message doesn’t actually require much handling. You just need the bytes of the image stuff into the HTTP.Request
object.
julia> blob = read("image.png")
69126-element Vector{UInt8}:
0x89
0x50
⋮
0x6f
0x35
0x63
The read
function just treats what it reads as a binary blob. It doesn't care what it actually represents. So the key is really just to get the Content-Type
set correctly.
req = HTTP.Request(
"POST",
"http://foobar/image",
["Content-Type" => "image/png"],
blob)
So imagine we sent this request to the server, and it needs to pull the image out. First we get hold of our blob of binary data:
julia> blob = HTTP.payload(req)
69126-element Vector{UInt8}:
0x89
0x50
⋮
0x6f
0x35
0x63
Julia has a generic system for loading any type of file offered through the FileIO
package. Others can extend the interfaces offered by FileIO
to support the loading of a larger selection of file types.
A key function is FileIO.load
, but this only support loading directly from file or from an IO
or Stream
object. You cannot load from a binary blob. Thus we need to wrap the blob in an IO
object. IOBuffer
to the rescue!
julia> io = IOBuffer(blob);
Unfortunately loading from an IO
object means the load system has to guess what filetype it is dealing with from the data discovered while reading. This seems unnecessary if we actually know what sort of data we are dealing with.
Thus FileIO
has introduced the concept of Stream
objects which you can think of as typed IO
objects. E.g. this is how I create a stream object for reading .png
files.
julia> stream = Stream{format"PNG"}(io)
The loader will know that it is dealing with .png
data without guessing.
julia> image = load(stream)
I will consider adding more info about how to work with multipart form data and images if there is interest in it.
Working With Forms
Here there are some gotchas. I misunderstood the use of forms by failing to observe a few important facts:
- When you click the submit button, web browser sends a GET request by default, not a POST request as I had assumed.
- You must give each input widget a
name
not anid
for data to actually be transported. Theid
attribute is used to refer to the HTML tag from CSS, not for transmitting data to the server.
If this seems abstract. Let us look at an example. We will create a page that looks like this, where we can send the name and race of some Middle Earth character to the server.
To create this form we write the following HTML code:
<html lang="en">
<body>
<form>
<input type="text" name="fname" value="Frodo"/>
<input type="text" name="lname" value="Baggins"/>
<select name="race">
<option value="elf">Elf</option>
<option value="hobbit">Hobbit</option>
<option value="orc">Orc</option>
<option value="man">Man</option>
</select>
<input type="submit" value="update"/>
</form>
</body>
</html>
Whether you are using a button, textfield or checkbox you use the input
tag. However the type
attribute will vary. To be able to transmit the data input to this widget to the server through an HTTP request object, you need to add the name
attribute such as name="fname"
.
The value
attribute gives a default value for the textfield, but is otherwise not important. The dropdown box is created with the select
and option
tags and work a bit different since you need a way of storing all the different possible choices.
To be able to create a request storing all these values you need the UI button with type="submit"
. Once you click this button a request will be made.
Above you see this expressed as a GET request. After the URL you get parameters listed as:
form.html?firstname=Frodo&lastname=Baggins&race=hobbit
Each key-value pair is separated by a question mark ?
. If we want to pass data as a POST request instead we need to modify the form
tag.
<html lang="en">
<body>
<form method="post">
⋮
</form>
</body>
</html>
If you click the update
button to submit the form you will no longer see the chosen values in the address bar. So how do you see the posted values? They are not part of the payload. We can however explore them with the various built in developer tools which come with different browsers.
Okay, so we got some basic idea of how to setup the HTML page and inspect its behavior using a Web browser. Now let us look at how we can process this info from Julia. It is worth knowing that form data will be sent from the browser as the MIME type application/x-www-form-urlencoded
. This is stored with the Content-Type
header key.
using HTTP, Sockets, URIs
body = read("form.html", String) # your HTML page
HTTP.serve(ip"127.0.0.1", 8080) do req::HTTP.Request
@show req["Content-Type"]
if req["Content-Type"] == "application/x-www-form-urlencoded"
payload = HTTP.payload(req, String)
dict = queryparams(payload)
@show dict
return HTTP.Response(200, body)
else
return HTTP.Response(200, body)
end
end
If you store your HTML page in form.html
, you can use this code snippet above to run a little web server on localhost
at port 8080
. Just direct your web browser to address: http://http://localhost:8080
.
Related Articles
- Explore REST APIs with cURL — Exploring various Unix tools which help you experiment with REST APIs.
- A Guide to Different Web Technologies — How is WebSockets different from Unix sockets. AJAX vs WebSockets, jQuery, React etc how do they all related and overlap?
- Notes on JavaScript DOM Tree — Looks at things like manipulating the DOM tree, querying the tree for particular nodes. Dealing with events.
- Reusable Web Components Notes — A way to define your own HTTP tags in JavaScript which allows you to create reusable GUI components.