Performing HTTP request in Pony

by Paweł Świątkowski
26 Jan 2017

Recently I got interested in Pony. It is an actor-based and compiled to native code language which show a really good design. Basically, looks promising.

As this is not my first niche language, I know that there are many downsides to such ecosystems. One of them, probably most important, is lack of packages or tutorials for “common operations”. You have to do the hard work yourself, by compiling fragments of available documentation and digging through the source code. The result can be really rewarding, maybe even worth a blogpost? This one is born out of this: the need to perform HTTP request in Pony.

Network requests are actually quite hard. We are used to them, writing our PHPs, Rubys or JavaScripts, because they are the very building blocks of web programming. But reality is not that cool. A lot of things happen when you perform such a request, a lot of side effect might occur. Especially for “safe” functional languages, this is not an obvious operation.

For Pony, there is an official example available. But I think it is a bit intimidating. It is “HTTP request done as right as possible”, but I did not want this. I wanted to perform it with as little code as possible and without using more complex language features. Here’s what I got:

use "net/http"
use "net/ssl"
use "files"

actor Main
  let _env: Env

  fun get_ssl_context() =>
    try
      let cert_file = FilePath(_env.root as AmbientAuth, "cacert.pem")
      SSLContext.set_client_verify(true).set_authority(cert_file)
    end

  new create(env: Env) =>
    _env = env

    let ssl_context = this.get_ssl_context()
    let url = "https://api.github.com/repos/ponylang/ponyc/issues"
    let method = "GET"

    try
      let client = Client(_env.root as AmbientAuth, ssl_context)
      let url' = URL.build(url)
      let handler = recover val this~handle_response() end
      let request = Payload.request(method, url', handler)
      client(consume request)
    end

  be handle_response(request: Payload val, response: Payload val) =>
    _env.out.print(response.status.string())

There are quite a few things going on here, I’d like to go through them step by step. So, first of all, I import some packages using a use keyword. Nothing fancy. Than I define Main actor, which is a bit special – it’s like main function in C (more precisely, its create constructor, which we will talk about later). First complex thing is a get_ssl_context function. Since I’m going to perform a GET on Github API (which only supports HTTPS), I need this for the request to be done. The cacert.pem file is taken from the httpget.pony example linked above.

Then comes the constructor I mentioned above. You can tell it from new keyword. Its only argument is Env, which is basically “the outside world”. Some variables are assigned and then a try block is started. This is required because there are many things that might go wrong inside it and compiler won’t let me proceed without explicitly mentioning that it might happen. Note that this kind of block is also used in get_ssl_context function, because I’m reading a file, which is also pretty unsafe stuff.

Inside the block, I create an instance of built-in Client from net/http, passing it fragment of env and SSL context. Then I build a URL from a string. The apostrophe there is nothing special, it is just part of the variable name, denoting that it is somehow connected to the original url variable.

let handler = recover val this~handle_response() end

Here come a bit of Pony “magic” (quoted, because it’s not really magic; it’s just being explicit). First of all, this~handle_response() it a partial application of handle_response function. Using recover ... end block creates an immutable reference to it. I also need to specify that it’s val reference, because otherwise it won’t go with Payload creation in the line below.

let request = Payload.request(method, url', handler)

Now I create a real payload of the request. The arguments are pretty obvious (I hope): a method, a url and a handler, which will be called asynchronously. The last part is important. In Pony, there are no blocking operations, so we have to have a handler here.

In the next line, the request if actually made by passing the payload to the client I defined before. consume keyword is kind of like a destructive read, by which we let the compiler know that it won’t be ever used again (which also makes it concurrent-safe automatically).

be handle_response(request: Payload val, response: Payload val) =>
  _env.out.print(response.status.string())

The last part is to define an actual handler. This is simple. It is a behaviour (not a function, but behaviour is basically an asynchronous function) that takes two arguments: a request and a response. All I do is to print response status to stdout.

Now, after I compiled and run the code, the response written was 403, which is not quite what I expected. To solve the mystery, I had to print the response body too. So I changed my handler to:

be handle_response(request: Payload val, response: Payload val) =>
  for chunk in response.body().values() do
    _env.out.write(chunk)
  end

After that, I saw this:

Request forbidden by administrative rules. Please make sure your request has a User-Agent header (http://developer.github.com/v3/#user-agent-required). Check https://developer.github.com for other possible causes.

So, it turns out that you can’t make requests to Github API without specifying User-Agent header. This was easily fixed by adding a line to the constructor:

let request = Payload.request(method, url', handler)
request.update("User-Agent", "pony")
client(consume request)

And now everything worked.

This was the simplest way I found to perform a HTTP request. After all it’s not as hard as it looked and I learned quite a few useful things about the language. I hope to explore it more in the future, for example with parsing JSON from the response and do something with it.

end of the article

Tags: pony

This article was written by me – Paweł Świątkowski – on 26 Jan 2017. I'm on Fediverse (Ruby-flavoured account, Elixir-flavoured account). Let's talk.