Integrating Self-Contained Systems - be careful with your SSIs!

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

Integrating SCSs at the UI layer / with nginx

Problem description

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:

Product details page breadcrumb

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”.

While

  • the client created/encoded the SSI URI correctly (in the example / was encoded as %2F: /breadcrumb/1002422142/Thierry%20Mugler%20MUGLER%20COLOGNE%20Eau%20de%20Toilette%20Splash%2FSpray%20300%20ml)
  • 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).

Problem analysis

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.

The solution

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

Check.

Conclusion

  • 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.

Comments