Exercise 2: Swapping Auth and Logging Order — Possible Solution ==================================================================== ORIGINAL ORDER: auth_middleware, THEN logging_middleware ------------------------------------------------------------ app_orig = build_chain([auth_middleware, logging_middleware], handler2) app_orig({"path": "/", "authenticated": False}) trace: ['auth check', 'auth REJECTED'] Auth is the outermost layer here, so it runs first and rejects the request before logging_middleware is ever reached at all -- logging's own "before" line never fires. SWAPPED ORDER: logging_middleware, THEN auth_middleware ------------------------------------------------------------ app_swapped = build_chain([logging_middleware, auth_middleware], handler2) app_swapped({"path": "/", "authenticated": False}) trace: ['before logging', 'auth check', 'auth REJECTED', 'after logging'] DOES LOGGING'S "BEFORE" CODE STILL RUN BEFORE THE REJECTION? ------------------------------------------------------------------ Yes -- 'before logging' appears first in the trace, confirmed directly. Once logging_middleware is the outermost layer, its own code always runs before whatever's nested inside it gets a chance to reject anything, regardless of what that inner layer eventually decides. THE GENUINELY SURPRISING PART, VERIFIED DIRECTLY ------------------------------------------------------------------ 'after logging' ALSO appears in the swapped trace -- logging's own "after next()" code runs even though the request was rejected and the real handler never executed at all. This isn't a bug in the demo; it's a real, structural consequence of the onion model itself: from logging_middleware's own point of view, next(request) is just a function call that returned a value (the string "401 Unauthorized"). logging_middleware has no way to tell, from that return value alone, whether it's looking at a real response produced by the actual handler or a short-circuited rejection produced by a layer nested somewhere inside it -- both cases look identical: next(request) returned, so the code after that call simply runs, the same as it always does. WHY THE ORDERING CHOICE MATTERS IN PRACTICE ------------------------------------------------------------------ Put auth outermost (the chapter's own original order) and an unauthenticated request is rejected before ANY other middleware's own code runs at all -- genuinely cheaper, and it means logging never records a request that was never really let in. Put logging outermost instead, and every request -- authenticated or not, accepted or rejected -- gets logged both before and after, which may be exactly the real behavior wanted (log every attempt, including rejected ones) or may be a genuine, easy-to-miss surprise if the goal was "only log requests that actually reached the real handler." WHY THIS WORKS AS AN ANSWER ---------------------------- It runs both real orderings and compares the actual traces rather than reasoning abstractly about what "should" happen, correctly answers the literal question asked (yes, logging's before-code still runs first), and surfaces the real, structural reason logging's after-code also still runs even though the request was ultimately rejected -- a genuine consequence of middleware ordering, not an edge case invented for the exercise.