Plan 9: REST requests using the filesystem (seriously)

Written in Apr 07, 2024 by Lucas S. Vieira
lucasvieira@protonmail.com

space-glenda.png

Figure 1: I can't just make a Plan 9 article without a Glenda picture, right?

Lately I've been porting the Minerva System, which was originally built using Rust, to Golang.

But you probably don't know what Minerva System is. Well, it is a project built solely with the purpose of study and… overengineering. It is somewhat modeled after an ERP – since I've spent more time maintaining ERPs and dealing with its many business rules than recommended for my health –, I thought that a microservice-like attempt at building an ERP was the best place to experiment with new technology I wished to learn. So I programmed it using the Rust language, and it was fun for a while.

However, ERPs require time and can be hard to deal with. More than that, using microservices "because I can" certainly made it infinitely harder. Add some Rust and gRPC… even more time needed.

But this project was fun nonetheless, and even spawned a few other crazy subprojects. I was recently looking at an old attempt of mine at building a GUI for the Minerva System, more specifically for the Rust version. It was built using a port of microui for an operating system called Plan 9. Because… well, I'm not really into front-end web development, so I might as well always do things the hardest way I can. :P

webfs-minerva-microui.png

Figure 2: I don't care what other people think, it looks good in my eyes. :^)

This GUI had two characteristics which I enjoyed exploring. First: it was built using Plan 9 C (which is basically1 C, but not POSIX-compliant). Second: to perform connections with the REST API, I used Plan 9's own filesystem for HTTP requests.

Let that sink in for a bit.

I did not install any libraries. I did not have any REST or HTTP clients at hand to perform requests. I also did not open any sockets (although Plan 9 doesn't have sockets2). I simply used a bunch of special directories and files which interact with a service running on the background.

This opens up a lot of possibilities, and this is also part of the Plan 9 philosophy. Got access to the filesystem? Can you see the resource in the filesystem also? Voilà, you get free access. I mean… maybe. I don't wanna just oversimplify things.

For this post, I wanted to explore a little bit of how the webfs service works and how one can perform HTTP requests using it, especially to a REST API just like I did before.

Exploring webfs

Let's start with the basics. According to its own manpage on Plan 9, webfs(4) is a tool that "presents a file system interface to the parsing and retrieving of URLs". There isn't much to explain about what it is, except maybe that it relies on the 9P protocol, which is kind of a big deal: even though the original developers of Plan 9 have moved on a long time ago, 9P is so important that it is currently used on modern features such as WSL2 on Windows3.

webfs(4) normally is also used with webcookies(4), which is another service for… well, managing cookies. But we won't need to work with cookies here, so we can ignore webcookies(4) now.

To run webfs(4), you'll need Plan 9 (of course), and an internet connection. On this old tutorial, I go step by step on configuring 9front (a fork of Plan 9 which is still being maintained) on a Raspberry Pi 3B+, which should provide a head start if you're interested. Afterwards, you just need to grab an IP for your machine on your local network:

ip/ipconfig

Finally, you can start webfs(4) (and webcookies(4), which should be started before webfs(4)).

if(! webcookies >[2]/dev/null)
        webcookies -f /tmp/webcookies
webfs

Generally, I put these commands on my $home/lib/profile file, so that every time I start a session or turn on my Raspberry Pi, I have both a working internet connection and easy access to web browsing on any web browser that depends on webfs (such as mothra(1) or abaco(1)).

webfs-mothra.png

Figure 3: This blog is 100% viewable on mothra(1), satisfaction guaranteed.

webfs-abaco.png

Figure 4: abaco(1) is old stuff that nobody should use, but may work on some cases.

webfs-netsurf.png

Figure 5: netsurf will make you a lot happier, but you'll have to compile it from source.

The /mnt/web directory

After you run webfs, you get a mount point at /mnt/web, which can be used as interface for our requests.

/mnt/web starts with an hierarchical directory, and if you didn't do anything yet, you should see only the clone and ctl files.

webfs-1.png

These files are special, in the sense that they're not simple text files. We should think of them as streams – interfaces to the webfs service.

Global parameters: the ctl file.

ctl is such a special file, which exists for maintaining parameters of the webfs service. We won't go deep into the purpose and usage of this file, so it should suffice to know that we can use it to set and retrieve global information such as request user-agent and request timeout. There is even a way to perform pre-authentication so you don't have to keep using Basic-type authentication all the time.

If you print the ctl file, the currently set parameters will be displayed on console.

webfs-2.png

Allocating a connection: the clone file

The most interesting file on this directory is the clone file. If we open it, then a new directory with a numbered name will be created under /mnt/web (I am going to refer to said directory as /mnt/web/n).

Let's start by allocating a connection. We then immediately cd into the n directory:

webfs-3.png

Figure 6: As you can see, in this example, n corresponds to the number 0, so whenever I talk about /mnt/web/n, I am referring to what is, on these screenshots, the directory /mnt/web/0.

So why did we cd into /mnt/web/n, you may ask? Well, because this directory represents our actual connection. We also do this to prevent the files in this directory from being considered unused, thus closing and recycling the connection4.

Performing requests

Now we can perform actual HTTP requests. For that, we'll use the PokéAPI which kindly provides a REST API for fetching Pokémon-related data.

To keep things simple, we're going to fetch a single Pokémon from the Pokémon list. This can be done through a GET request to https://pokeapi.co/api/v2/pokemon?limit=1offset=0.

Right now, the file /mnt/web/clone is equivalent to /mnt/web/n/ctl in the sense that these files can be used to control our requests, but we're going to use /mnt/web/n/ctl for a visual effect.

First things first, we simply inform webfs that we want to perform a GET request to the given URL. This can be done through two parameters, and we pass these parameters to webfs by writing them, one by one, to /mnt/web/n/ctl.

webfs-4.png

NOTE: If we were performing a request that needs a body, such as a common POST or PUT request would require, right now we could write the body to the postbody file. No special magic required here, just be mindful of the data you write there.

Extracting request data

Let's stop here for a moment. We're ready to perform our request, but we can also use webfs(4) to parse our URL for us if we're using, for example, rc(1) to execute a script that does something web-related.

The parts of our request can be seen on the /mnt/web/n/parsed/ directory. This directory contains a lot of files which are rather useful.

webfs-5.png

Let's explore these files in greater detail:

  • fragment: The portion of the URL separated by the # character, if existing (e.g. if you're jumping to a certain heading on the current page while browsing).
  • host: Host address of the server.
  • user, passwd: Basic authentication data, if informed.
  • path: Route of resource on host, if informed.
  • port: Port you're trying to access on the server, if informed.
  • query: Query parameters; the portion separated by the & character, if existing (e.g. if you're trying to pass pagination parameters through the URL).
  • scheme: HTTP, HTTPS, etc.
  • url: Echoes back the used URL.

webfs-6.png

Performing the actual request

The next step is performing the request. Again, there is no magic here: just open the file /mnt/web/n/body for reading. This is a once-per-request operation, and it will reset our request parameters after it is done:

webfs-7.png

Let's take a better look at the response. I've isolated and formatted what was echoed:

{
    "count": 1302,
    "next": "https://pokeapi.co/api/v2/pokemon?offset=1&limit=1",
    "previous": null,
    "results": [
        {
            "name": "bulbasaur",
            "url": "https://pokeapi.co/api/v2/pokemon/1/"
        }
    ]
}

Ok, that seems pretty good. But wait, what if we wanted to view the response again?

webfs-8.png

Oops. It attempts to perform the request once again, but now we need to supply request data once more (as you can see, it complains that no url is set).

Exploring response headers

Let's do everything all over again. We'll supply all the parameters, perform the request, and then we'll look again at our /mnt/web/n/ directory.

webfs-9.png

Uh, what the heck, where did all these files come from?

Well, it's pretty simple: all the response headers are parsed just like our request parameters were parsed in the parsed directory. But for the response, webfs simply creates a new file on /mnt/web/n/ for each received header.

You'll also notice that the headers have funny names. This is because they were stripped of hyphens (-), so a header such as access-control-allow-origin would become accesscontrolalloworigin.

For the sake of comparison, let's take a look at those headers when we perform such a request on Linux by using curl.

curl https://pokeapi.co/api/v2/pokemon\?limit\=1\&offset\=0 -vvv

webfs-curl-compare.png

You'll find on the screenshot above that the request data are the lines beginning with an >, and the response data begins with a <. Among the response lines, after < HTTP/2 200, you'll see every response header received.

What if something goes wrong?

Suppose that we attempt to perform a request to a resource that does not exist:

webfs-10.png

Just like in the example where no URL was set, we get an error. But this time, we can see that the error string is a bit different – we actually get an error code we can deal with.

To work with this error string, we'd have to get output from the stderr stream (as opposed to the default, stdout, which is basically the main console output). If you're using the C language, Plan 9 C has useful language functions such as errstr(2) and constants such as ERR_MAX for handling these – and trust me, this is easier to do in C than it seems at first sight.

So now you can probably work with most request with ease, even handling HTTP error codes.

Conclusion

I always thought that this approach was an inspiration. Of course using the filesystem has its problems, especially if you're looking for speed, but 9P is still a very good protocol that provides decent speed for most cases.

Using the filesystem as a web interface is also interesting in the sense that this is language-agnostic. You only need to run a specific service, and this service works for any application that might depend on it. It does what it should do – and does it well.

Of course, there are flaws on webfs(4). For example, I am still not sure what to do when you want to handle other HTTP codes for success cases, e.g. if you're returning 201 Created for a resource. In these cases, the body file will open just fine, but the actual HTTP code goes to the limbo. I guess this is irrelevant for most cases, but this small feature is missing, after all. Unless I'm the one who missed something from the manpages.

Furthermore, handling HTTP error responses is a little complicated on my end, since I generally design my APIs to return response bodies on error cases. This is one more thing I simply could not do at all: retrieve the response body when the API returns an HTTP error code. stderr is all you get and that's it.

Welp, easier said than done, right? Though I suppose it can't be too hard to make patches for 9front fixing these issues. I might just do that whenever I can.

Footnotes:

1

By "basically", I mean that it's fine for anyone acquainted with C development. But C development in Plan 9 is still different enough than "ordinary" C for compilers such as GCC or Clang, and why not say this, is also much simpler.

2

You really don't get access to sockets in Plan 9, at least that I know of. For your networking needs in C, you'll be using dial(2), and frankly, it is much more elegant.

3

As stated here, WSL2 has been modified to start a 9P server, with Windows acting as a client. This is how you're able to access your WSL's files from Windows Explorer.

4

According to webfs(4) manpage, the connection is recycled when all the files in /mnt/web/n are closed, so I suppose cd'ing into /mnt/web/n prevents that at this point. But I am not sure of that… I couldn't really find anything on this behaviour on the rc(1) shell manpage, but it seemed to work, so bear with me for a while.

Back to last page