Path resolution in Quarkus

We have had an unusually bumpy ride the last few weeks. Path resolution can be sneakily complicated, and in trying to make things better, we accidentally made them worse! We’ve fixed it all now, but you might notice some changes. Hopefully, this post will make clear what those changes are, what they mean, and what you can do to put everything back the way you want it.

TL;DR: As of 1.11.5.Final and 1.12.1.Final, leading slashes matter in config. So if you use /endpoint, that endpoint will be served from the absolute root. If you want it relative to some containing bucket, omit that leading slash. For example, quarkus.http.non-application-root-path is now q by default, which nests it under quarkus.http.root-path, matching the original behavior. You can use an absolute path, e.g. /q, to serve non-application endpoints from the absolute root (as a sibling of the http root, if that is set). To remove the non-application endpoint behavior entirely, set quarkus.http.non-application-root-path to the same value as quarkus.http.root-path. The most foolproof way to do this is using a variable: quarkus.http.non-application-root-path=${quarkus.http.root-path}.

The long story

Once upon a time, Quarkus defined additional endpoints for things like health checks and metrics. They were served from the quarkus.http.root-path alongside any endpoints the application defined. This isn’t always obvious, as quarkus.http.root-path is / by default, making it effectively invisible.

As these proliferated, we started worrying about polluting the application endpoint namespace, and thinking about how we could group these non-application endpoints together to avoid colliding with application endpoints and make it easier to deal with security and access policies. Some users further asked if we could serve these non-application endpoints from another port entirely (we haven’t gotten there yet).

The first step was to group all of these extension-defined endpoints together. This was the genesis of the non-application endpoint path. The default location of this new path was /q, and it was nested under the HTTP root path, just as the other endpoints had been. The effect was to move /health to /q/health, as an example.

We knew that moving some of these endpoints, like metrics and health, would be problematic for already deployed applications and human muscle memory. To ease the transition, we added redirects for some of these endpoints, so that if you visited /metrics you would be redirected to /q/metrics.

Non-application endpoint support shipped in 1.11.0.Final.

And then things started to go sideways. Some cloud hosting providers only accept 200 as a definition of health, for example, so the redirect (a 301) didn’t have the intended effect. There was also some confusion about how to turn the non-application endpoint off to return to previous behavior, and further questions about how to move specific endpoints out of this non-application endpoint collection.

As an aside, how we got into this situation is not helped by differences in how libraries behave. Vert.x always wants segments beginning with leading slashes when creating routes, for example, while JAX-RS effectively ignores leading slashes in @Path annotations. Anyone used to Vert.x always adds leading slashes, and anyone using JAX-RS just does whatever and it magically works.

In Quarkus, an implementation detail was exposed by accident: non-application endpoints defined by extensions are based on Vert.x routes. Default path configuration values started with slashes to enable quick route creation and allow simple append behavior. There wasn’t anything in the early days of Quarkus to suggest this was a bad idea, and developers with experience in JAX-RS didn’t have any warnings one way or the other, because JAX-RS handles it.

Now, however, we were in the situation where paths weren’t being resolved as people expected, and the configuration changes required to resolve that situation either weren’t intuitive or lead to other problems. We ended up putting all of the possible configuration permutations into a spreadsheet so we could see side by side what happened when you combined different configuration values. The results were not awesome. However, the exercise allowed us to step back and look at the big picture to evaluate what needed to change to allow application and non-application endpoints to behave as you need them to.

While the set of configuration attributes used to configure paths in Quarkus remains unchanged, how configured values are interpreted is different:

  • Endpoint path configuration defaults are now relative values. /q is now q, /metrics is now metrics, etc. This means that, out of the box, these endpoints will resolve relative to the containing root, which is what JAX-RS does per spec, and is what we believe most users intuitively expect.

  • Leading slashes in explicitly configured values matter. We know some of you want to move endpoints to specific places, and the most consistent way to express that intent is to allow you to specify the exact uri you want an endpoint to use. If you specify /metrics, that is where you will find the metrics endpoint.

These updates have been made available and the dust should all have settled with 1.11.5.Final and 1.12.1.Final.

Note that convenience redirects for non-application endpoints are still present, but they can be disabled by setting quarkus.http.redirect-to-non-application-root-path to false. That hasn’t changed at all.

Resolution of configured paths

Let’s go through some examples of how paths resolve using our new rules. We’ll start with the following assumptions:

  • We have a Hello World application that defines @ApplicationPath("/hello")

  • The application specifies two endpoints using @Path("world") and @Path("/aliens")

The configuration attributes we care most about are:

  • quarkus.http.root-path - The HTTP root path. All web content is served relative to this root path.

  • quarkus.http.non-application-root-path - The non-application endpoint root path.

We’ll also highlight some configurable non-application endpoints of interest:

  • quarkus.micrometer.export.prometheus.path - The location of the micrometer metrics endpoint.

  • quarkus.smallrye-health.root-path - The location of the all-encompassing health endpoint.

  • quarkus.smallrye-health.liveness-path - The location of the liveness endpoint.

Let’s look at what happens when we start pulling levers. In the examples below, pay attention to punctuation in config, as that will be the key to why things behave the way they do.

Defaults

Here are the default configuration values:

  • quarkus.http.root-path=/

  • quarkus.http.non-application-root-path=q

  • quarkus.micrometer.export.prometheus.path=metrics

  • quarkus.smallrye-health.root-path=health

  • quarkus.smallrye-health.liveness-path=liveness

That configuration (combined with the declared application endpoints) leads to the following valid URLs if our Quarkus application is running in dev mode:

Note that the quarkus.http.root-path is hiding in this example, because its value is /.

There are convenience redirects in this case as quarkus.http.non-application-root-path is not the same as quarkus.http.root-path. In this configuration, /metrics will be redirected to /q/metrics.

Change the Http Root path

Let’s change the HTTP root path to /root so the impact it has on resource resolution is visible:

  • quarkus.http.root-path=/root

  • quarkus.http.non-application-root-path=q

  • quarkus.micrometer.export.prometheus.path=metrics

  • quarkus.smallrye-health.root-path=health

  • quarkus.smallrye-health.liveness-path=liveness

This results in the following dev mode URLs:

There are convenience redirects in this case, too, as quarkus.http.non-application-root-path is not the same as quarkus.http.root-path. In this configuration, /root/metrics will be redirected to /root/q/metrics. This is consistent with previous behavior, where non-application endpoints were implicitly relative to the HTTP root path.

Move the non-application root path (/q)

Let’s try something we couldn’t do before. We’ll move the non-application endpoint outside of the HTTP root path by specifying an absolute path, /q:

  • quarkus.http.root-path=/root

  • quarkus.http.non-application-root-path=/q

  • quarkus.micrometer.export.prometheus.path=metrics

  • quarkus.smallrye-health.root-path=health

  • quarkus.smallrye-health.liveness-path=liveness

This results in the following dev mode URLs:

There are still convenience redirects in this case, as quarkus.http.non-application-root-path is not the same as quarkus.http.root-path. Redirected URLs are still relative to HTTP root, so /root/metrics will be redirected to /q/metrics.

Move individual non-application endpoints (/metrics and /liveness)

This is is another configuration that was not previously possible. We can individually move configurable non-application endpoints to a specified absolute path, specifically /metrics and /liveness in this example:

  • quarkus.http.root-path=/root

  • quarkus.http.non-application-root-path=/q

  • quarkus.micrometer.export.prometheus.path=/metrics

  • quarkus.smallrye-health.root-path=health

  • quarkus.smallrye-health.liveness-path=/liveness

This results in the following dev mode URLs:

There are still convenience redirects in this case, as quarkus.http.non-application-root-path is not the same as quarkus.http.root-path. However, these redirects only apply to non-application endpoints controlled by the non-application endpoint root. We’ve essentially removed the metrics and liveness endpoints from that root, so they won’t be redirected. In this configuration, if you request /root/health, it will be redirected to /q/health. A redirect will not be provided for /root/health/liveness or /root/metrics.

Remove the non-application endpoint root

Some of you have asked how to turn this non-application endpoint root stuff off entirely. A clear expression of your intent is best. To disable the non-application endpoint, make it identical to the HTTP root path. In essence, you are telling the runtime to "serve all non-application endpoints from the HTTP root". This example uses a variable to ensure the values remain the same:

  • quarkus.http.root-path=/root

  • quarkus.http.non-application-root-path=${quarkus.http.root-path}

  • quarkus.micrometer.export.prometheus.path=metrics

  • quarkus.smallrye-health.root-path=health

  • quarkus.smallrye-health.liveness-path=liveness

This results in the following dev mode URLs:

There are no convenience redirects in this scenario, as the non-application endpoint behavior has been disabled entirely.

Knock-on effects

For the most part, we hope this will be transparent. We discovered some very inconsistent path handling along the way, which lead us to believe that many (or even most) of these values are never customized.

You are most likely to see a behavior change if you have customized the HTTP root path. In that case, we hope the new rules and examples above will help you understand how to tweak your configuration to get everything to behave the way you want it to.

Extension writers will see the biggest change. The Writing extensions guide has been updated to describe changes to the build items used to create non-application endpoints. The general rule, however, is to avoid constructing your own endpoint paths, and rely on NonApplicationRootPathBuildItem and HttpRootPathBuildItem to construct them for you.

Parting thoughts

While we know it is impossible to make everyone happy, we hope we have at least managed to acheive a pattern for configuration that leads to more predictable and consistent results. And we apologize (again), for any behavior changes you may have observed as we sorted this out.