Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix Route merge order to fix Issue #3066 #3160

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open

Conversation

exi
Copy link

@exi exi commented Sep 23, 2024

In case of duplicate routes, the old logic gives precedence to older routes, which breaks cases where the routes are updated with new environments and re-added.

This gives precedence to the newer ones.

/claim #3066

@CLAassistant
Copy link

CLAassistant commented Sep 23, 2024

CLA assistant check
All committers have signed the CLA.

Copy link
Collaborator

@kyri-petrou kyri-petrou left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little bit confused. ++ should be left-associative, so why / how does this work exactly? AFAICT this should be breaking things 😕

Also, can you please add the reproducer as a test that was added in the original issue?

EDIT / PS: If this is indeed the solution to the issue, I think we should be fixing ++ instead and adding tests to ensure that it's left-associative

@exi
Copy link
Author

exi commented Sep 23, 2024

@kyri-petrou

EDIT / PS: If this is indeed the solution to the issue, I think we should be fixing ++ instead and adding tests to ensure that it's left-associative

As far as understand, doing ++ on routes just concatenates them in a list. That list is then checked left-to-right during calls to search for appropriate request handlers.
Since in the linked issue, we add the same route twice, with different environments, before this PR the old route takes precedence, after this PR, the new one does.

I agree that I would have expected routes to be merged route-by-route, but figured that there probably was a good reason to just put then in a list.

@exi
Copy link
Author

exi commented Sep 23, 2024

Adding the test case is complicated because it depends on TestServer, which is afaik not usable in zio-http because it would lead to circular dependencies.
For my local testing I added a new test top level sbt module named zio-http-regressiontests. I'm happy to include it but it seems a bit misplaced since it only contains one test.

@987Nabil
Copy link
Contributor

Even if this works, this is not how ++ should work. Like on Map the right should win. If there is a bug, it is way deeper in the code

@exi
Copy link
Author

exi commented Sep 23, 2024

@987Nabil Do you mean that the request handlers should be constructed from the routes iterating back to front over them or do you mean that the request handler tree building code should read front to back but override older values or at least prefer newer ones?
I could implement that.

@exi
Copy link
Author

exi commented Sep 23, 2024

@987Nabil @kyri-petrou

I updated the PR to change the handler merging logic in cases of multiple handlers for the same path.
The old code iterated over the handler from left to right, the new one flips that and also gets rid of explicit loops in favor of a reduction.

@exi
Copy link
Author

exi commented Sep 23, 2024

Since the new code is more general, i also got rid of the special casing between one handler and more than one handler.

In case of duplicate routes, the old logic gives precedence to older routes, which breaks cases where the routes are updated with new environments and re-added.

This gives precedence to the newer ones.

/claim zio#3066
@exi
Copy link
Author

exi commented Sep 23, 2024

Lot's of tests failing, I'll check what's up with that.

@987Nabil
Copy link
Contributor

This change does not look right either. Neither the place nor the fact that we replace a while loop with allocation of an interator

@exi
Copy link
Author

exi commented Sep 23, 2024

@987Nabil Then i'm not sure what the intended behaviour is.

Routes uses the normal ++ behaviour, which when doing oldApp ++ newApp, will create Chunks of routes and put the new one at the end/right.
This later gets dumped into then handler tree building where routes are aggregated by path, while still keeping the chunk order of the handlers.
The handler tree building at the lowest level creates the real handler where in the case of the code I changed here, the old code only ever looks at the second handler in the list when the first handler returned Status.NotFound.

And since the chunks and therefore the handlers are put into "oldest left" order and the handler building iterates left-to-right, the oldest one gets preference every time.

Alternative

I could change the ++ implementation for Routes and do an actual merge instead of a concatenation, and throw away the old handler in case of duplicate paths. Some other ++ implementations seem to be doing that.

@exi
Copy link
Author

exi commented Sep 24, 2024

@guizmaii @jdegoes I went through a couple of iterations in this PR already but I'm now a bit lost on what the desired solution is.
I think the key issue is that ++ for Routes just appends the new routes to the list of old routes and the handler building walks this list front-to-back and uses whatever handler works first.
I could either change the handler building logic to work backwards, or I could implement a different ++ for Routes where that discards old handlers during the merge. Which one is the preferred solution here?

@exi
Copy link
Author

exi commented Sep 24, 2024

@kyri-petrou @987Nabil Turns out, our notion of how ++ is supposed to work is wrong for Routes. I just found this comment:

* Combines this HTTP application with the specified HTTP application. In case
* of route conflicts, the routes in this HTTP application take precedence
* over the routes in the specified HTTP application.
*/
def ++[Env1 <: Env, Err1 >: Err](that: Routes[Env1, Err1]): Routes[Env1, Err1] =
copy(routes = routes ++ that.routes)

It seems like "old (left) has precedence over new (right)" is the intended behaviour.
Which makes addRoutes for the same route unusable during tests.
I also think that many of the tests zio-http has that use addRoutes do not expect this behaviour.

@987Nabil
Copy link
Contributor

@jdegoes I find this behaviour confusing. Everywhere else we give "the new thing" precedence. Should we change it?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants