NGINX routing

So, you want to deploy your new heavy-math service. You'll host this in your nginx web server, because it's the bester best practice. Your julia code looks like this:

using HTTP: HTTP

function heavymath() end

router = HTTP.Router()
HTTP.register!(router, "GET", "/**", heavymath)
HTTP.serve!(router, "0.0.0.0", 1235)

Pretty simple, right? We're listening to port 1235 of the host computer and we're accepting all incoming connections. We pass all the path to heavymath. We'll do the API design in nginx, all good.

On the nginx side:

location /api/v1/heavy-math/ {
    proxy_pass https://127.0.0.1:1235;
}

That means that the users on the internet, who will be seeing nginx and not julia, won't hit 0.0.0.0:1235 (obviously), but https://your-nginx.com/api/v1/heavy-math/

So, if you do HTTP.get("https://your-nginx.com/api/v1/heavy-math/plus/1/1"), your julia code will receive /plus/1/1 and execute the heavymath function. Perfect, end of blog post.

But these are on the same container? Is that a best practice?

You are a great programmer! You like things tidy. You're using docker and nginx is in a separate container from julia. You've set everything up so if julia explodes, docker restarts it and users don't notice a thing! Your julia container even has its own hostname in the docker network!

You want to keep things tidy, so you log this hostname in the nginx config:

set $heavymath_hostname https://heavymath:1235;

location /api/v1/heavy-math/ {
    proxy_pass $heavymath_hostname;
}

😌😌😌 so tidy!

But! when you run this, julia now fails:

If you do HTTP.get("https://your-nginx.com/api/v1/heavy-math/plus/1/1"), your julia code will receive /api/v1/heavy-math/plus/1/1 and return 404.

What? 🤔

Let's ignore nginx's opinion that this is a bad idea altogether, and let's see what is happening.

NGINX's docs are notorious for their information density, making some cases really easy to miss.

From the proxy_pass directive:

A request URI is passed to the server as follows: [...]

In this case, if URI is specified in the directive, it is passed to the > server as is, replacing the original request URI.

But we're not using a request-specific variable! We're using a constant! Well, no. We aren't and there is no real distinction between these! So the proxy_pass sends the whole URI upstream.

At this point, we have two options

  1. Change the julia code to accept user-facing URIs

  2. Use some nginx magic to emulate the behaviour before

  3. ~Stop (ab)using nginx runtime variables~

Option 1 is simple and would look like this

using HTTP: HTTP

function heavymath() end

router = HTTP.Router()
HTTP.register!(router, "GET", "/api/v1/heavy-math/**", heavymath)
HTTP.serve!(router, "0.0.0.0", 1235)

But that introduces heavy coupling between the problem we solve and the deployment. So let's see how option 2 looks like:

location ~ ^/api/v1/heavy-math/(.*) {
    proxy_pass  $heavymath_hostname/$1;
}

Regex to the rescue! That fixes it right?

Well, yes, until you realize that regex matching is URL decoded!

That will definitely break your HTTP.jl code, which will complain with an obscure :INVALID_STATUS_LINE (which basically means that the HTTP message doesn't match the HTTP spec. It doesn't, because the target is not URI encoded)

Let's re-encode it:

location ~ ^/api/v1/heavy-math/(.*) {
    set $path $1;
    proxy_pass $heavymath_hostname/$path;
}

Awesome! That now works.

That is, unless you have also passed GET parameters!

Let's add these in too:

location ~ ^/api/v1/heavy-math/(.*) {
    set $path $1;
    proxy_pass $heavymath_hostname/$path$is_args$args;
}

Now that's equivalent! Note that this is quite significantly slower than the 'static' version (but also that "slower" for nginx is still pretty fast - so it's probably OK).

Conclusion

NGINX is one of these tools that you need to read its docs 10 times to understand what really is going on. Invest some time early on to build a solid templating for your nginx server, to get the right mix between flexibility and make sure your rules are readable by humans. Comments in the source code help a lot!

Have fun building! 👷🏾‍♂️👷🏾‍♀️🏗️

© JuliaHub
Website built with Franklin.jl and the Julia programming language