Thanks for attaching the test case. I debugged it and I think I found the root cause of your problems.
In evaluate() the join class adds the operation as a pre-operation to the evaluation context. So Lifecycle.handled() will be executed before the perform() operation of the rule gets called which means that perform() won’t be executed at all. Therefore you get a 404.
@lincoln: Wouldn’t it make sense to let Join always call Lifecycle.handled()? I don’t think it makes sense that other rules can also match after a join. Perhaps we could call Lifecycle.handled() by default and let users override this behavior using something like this: