Your middleware is noncommutative

Any half-decent programmer knows that in almost all languages, functions are non-commutative. The order in which you call two different functions matters in general.

But people forget this frighteningly often specifically when dealing with middleware in web servers. (An example of applying middleware is altering every response to return the appropriate CORS headers, because it would be a massive pain in the ass to manually do that for every response.)

Let me give you two examples. First I was trying to update Glee’s dependencies. You may as well just read the commit message:

Update dependencies

Important changes:
- fallback (i.e. 404) is no longer into_service()
- axum::Server is gone, need to use tokio::net::TcpListener as in the
  axum example

  	https://docs.rs/axum/latest/axum/index.html#example
- as Form is an Extractor that might consume the request body, we must
  put the Form extractor *last*.

	https://docs.rs/axum/latest/axum/struct.Form.html
	https://docs.rs/axum/latest/axum/extract/index.html#the-order-of-extractors

	Important to remember from second link: *the order of extractors
	matters*. They are applied left-to-right and then the request is
	fed, and *generally extractors do not commute*.

You don’t say, right? But I managed to chase this error for two hours before realizing that my middleware was non-commutative.

Today I was working on a web project for K-Scale Labs. We had been facing perpetual, mysterious CORS errors. I spent several hours wondering if, despite the configuration for CORS being piss-easy in FastAPI, I had somehow managed to mess it up. It turns out that my API calls were incorrectly formatted. Fixing them magically made my CORS errors go away. What on earth was going on?

My leading hypothesis as of half an hour ago was “some inexplicable bullshit is going on that I will never understand”. Only then did I actually decide to look at how the CORS middleware was set up in the context of the rest of the app, not just the one or two lines that set up the CORS middleware itself.

Oh. The project was passing every request to the appropriate handler BEFORE applying the CORS middleware, and those requests would return appropriate error responses by RAISING AN EXCEPTION. My CORS configuration was perfectly fine. Instead, every actual error was being swallowed by a CORS error because the webserver raising an exception stopped execution before the CORS middleware could be applied.

And I overlooked all of this because this code and the structure were not set up by me.1 I was just taking it all for granted. There is a lesson here: confusing things are very often the cause of non-commutative middleware, especially when you don’t understand the structure of the overall web application.

In some ways this is an obvious principle. If I asked you explicitly, “do your middleware commute”, well of course they don’t. But you don’t have a fairy godmother hovering over your shoulder asking “do your middleware commute” at the exact right times. You are bound to forget once or twice, which is precisely what makes this class of problems so irritating.

But you can do the next best thing: anytime anything is going wrong and you can’t figure out why, you should always ask yourself if the cause is non-commutativity. Be your own fairy godmother. Non-commutativity in general is the most rich source of bugs and unpredictable behavior. Even more so when it comes to web projects, which tend to have a lot of complexity.


  1. I have strong objections to the idea of “raise an exception and convert that into an error HTTP response (i.e. a 4xx or 5xx response)”, among many other parts of the architecture that I do not have control over. Largely because deliberately cutting execution short without thinking about the consequences tends to make these non-commutativity bugs more pernicious in the first place!↩︎