In this post I’m going to describe an issue we experienced with nginx and its handling of Server Side Includes (SSIs). We saw that nginx at first decodes the SSI URI path and afterwards encodes it when loading the resource. And in some cases, the URI path encoded by nginx was different than the original one. The solution is easy (use query parameters if in doubt), but I thought I’d share this so that others maybe don’t run into this issue and/or see how to debug such things.
At first, in which cases are SSIs useful?
In our case, we’re using SSIs to integrate Self-Contained Systems (SCSs) in the UI layer: one SCS is responsible to process a certain request, and adds SSIs for parts of the page that are “owned” by other SCSs. The following picture shows the basic flow:
- the http client / browser requests a page (“/page1”)
- nginx passes the request to the SCS responsible for this location (“SCS-1” in the example)
- SCS-1 returns a page that contains an SSI (
<!--# include virtual="/navigation" -->)
- nginx resolves the SSI by loading the resource from the responsible SCS (the upstream “behind” the location, “SCS-2” in the picture)
- nginx replaces the SSI with the response body returned from SCS-2
- nginx returns the complete page to the client
While in the example above the navigation has a static URI, there are other SSIs with a dynamic URI: the product details page for example also integrates a breadcrumb for the product:
The service that provides this breadcrumb snippet defined the related Play! Framework route with two path segments, one for the product id and another one for the product name (which should be displayed as last part/crumb):
GET /breadcrumb/:productId/:name BreadcrumbController.breadcrumb(productId: String, name: String)
The client SCS then would write the following SSI to include the breadcrumb for the product 4223 with name “Dockers Shoes Boots, Leder”:
<!--# include virtual="/breadcrumb/4223/Dockers%20Shoes%20Boots%2C%20Leder" -->
Unfortunately, for a few products the details page did not show the breadcrumb - weird! This was the case when the supplied name contained special characters like a backtick (
\`) or slash (
/), as it’s the case for “MUGLER COLOGNE Eau de Toilette Splash/Spray 300 ml”.
- the client created/encoded the SSI URI correctly (in the example
/was encoded as
- and GETting this URI returned the expected breadcrumb snippet
the SSI did not trigger the expected request (the action method
BreadcrumbController.breadcrumb(productId, name) was not invoked).
Activating nginx debug logging showed interesting things (on my dev box I set
error_log /var/log/nginx/error.log debug;). This is an excerpt from the logs for the breadcrumb SSI for the product mentioned above:
This shows a final proxy request
GET /breadcrumb/1002422142/Thierry%20Mugler%20MUGLER%20COLOGNE%20Eau%20de%20Toilette%20Splash/Spray%20300%20ml HTTP/1.1 which contains the unencoded slash (in “Splash/Spray”) instead of the percent encoded
%2F. Because the Play! route defined two path segments (for productId and name), this request coming with three path segments did not match any route so that Play! returned 404.
Similarly, the debug logs for a product with a backtick in the name (“De`Longhi Cappuccino Thermo-Gläser doppelwandig 2 Stück”) show that the backtick was sent unencoded to the upstream:
Here we see that Play! rejected this request with 400 - it reports the backtick as illegal character.
Because nginx does not decode/encode query parameters for SSIs, we can change the related route definition to accept the name as a query parameter. That nginx passes query parameters unmodified can be verified with the debug logs. For the SSI
<!--# include virtual="/breadcrumb/1002422142?name=Thierry%20Mugler%20MUGLER%20COLOGNE%20Eau%20de%20Toilette%20Splash%2FSpray%20300%20ml" -->
the debug logs show
- If you provide a service/resource for inclusion via SSIs and the resource contains dynamic parts that might need encoding then prefer query parameters over path segments.
- If you’re experiencing weird things and nginx is involved then nginx debug logs might help.