-
Notifications
You must be signed in to change notification settings - Fork 154
Migrate orderbook API from warp to axum #4080
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
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Code Review
This pull request migrates the orderbook API from Warp to Axum, modernizing the web framework and improving code organization. The changes include replacing Warp filters with Axum Router, migrating API endpoints, updating dependencies, and adding an E2E test suite. The review focuses on identifying critical and high severity issues, with actionable suggestions for improvement, while adhering to the repository's stricter code review style guide. One comment was modified to explicitly reference a relevant repository rule.
| response.status(), | ||
| StatusCode::NOT_FOUND, | ||
| "Expected 404 for invalid OrderUid ({description}): {uid}" | ||
| StatusCode::BAD_REQUEST, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Strictly speaking axum using correct HTTP error codes makes this migration backwards incompatible. I'm not saying we should keep the wrong ones, but we might wanna ping the FE folks. 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pinged them, though I'm not expecting these error codes to be especially problematic
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
:chefskiss:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a public API. Not only FE uses it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If needed I can go back, no worries. On the other hand, if clients are checking for a very specific error they, uh, shouldn't
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing is that we have many integrations, and who knows how they use the API?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd rather create a separate issue for this. Notify everyone we can, and only then merge the breaking changes separately.
This commit migrates the orderbook HTTP API from the warp web framework to axum. The migration includes: - Replace warp filters with axum Router and handlers - Update HTTP status codes to match axum conventions (422 for JSON deserialization errors, proper 400/404 distinction) - Restructure API with centralized AppState shared across handlers - Implement axum-compatible middleware for metrics and tracing - Remove warp dependency and related tracing utilities - Update e2e tests to reflect new HTTP behavior Breaking changes: - Invalid JSON payloads now return 422 (Unprocessable Entity) instead of 400 - Path parameter validation now returns 400 for malformed values - Error response format remains compatible 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
Fixed critical issues identified in PR review: - Fixed route conflicts by using .merge() for multiple HTTP methods on same paths - Changed get_trades_v2 to use database_read instead of database_write - Reorganized routes alphabetically by prefix using .nest() for better maintainability - Made METRIC_LABELS a module-level const for reusability - Removed convert_json_response helper (too simple to warrant abstraction) - Optimized Arc wrapping by using trait references directly - Applied per-method middleware before merging to ensure correct metric labels - Simplified test code by removing unnecessary verbose match expressions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
crates/orderbook/src/api.rs
Outdated
| axum::routing::get(get_user_orders::get_user_orders_handler) | ||
| .layer(middleware::from_fn(with_labelled_metric("v1/get_user_orders"))), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we reduce the usage of with_labelled_metrics? Given that axums paths just use a simple string to define them this should be easier than before even.
I think a structure similar to what we had before would be nicer to read. Not sure if this possible though since these handlers all have different types so some boxing or so would probably be needed. 🤔
let routes = [
("v1/solver_competition/latest", axum::routing::get(handler)
];
routes.into_iter().map(|(path, handler)| {
with_labelled_metric(path, handler)
})
.collect()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a small issue with your approach — not all routes match their label 😭
IMO we should make them match the label and in that case, we get better, we should be able to apply a middleware wholesale because we can get the path with placeholder
We might be able to simplify further with the TraceLayer and MatchedPath too
https://docs.rs/axum/latest/axum/extract/struct.MatchedPath.html
In summary: if the team is ok with the rename (I am!) we can make this whole thing even simpler
We can also move with this as is, and rename later so we are sure nothing broke
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MatchedPath sounds good. I'm fine with slightly renamed metric labels to make all of this simpler.
so we are sure nothing broke
I'm pretty sure those metrics are only used in the api latency dashboards which just shows all the labels that exist.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 60a3e7a, not tested yet though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this in our API docs anywhere that we have to update aswell? |
45e0f9a to
9af007d
Compare
|
Reminder: Please consider backward compatibility when modifying the API specification.
Caused by: |
| let mut storage = MockSolverCompetitionStoring::new(); | ||
| storage | ||
| .expect_load_competition() | ||
| .times(2) | ||
| .returning(|_| Ok(Default::default())); | ||
| storage |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not continue using the mocked storage? The test now only validates the conversion of the result.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Before we were mocking just the single trait, now we'd need to mock the whole app state
I can create an extra layer that further extracts the trait component out of the state and that does the test instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably makes sense if other e2e tests don't cover this already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've reviewed this and they are indeed tested in e2e, for example:
services/crates/e2e/src/setup/services.rs
Line 832 in 9a0f5fe
| .get(format!("{API_HOST}/api/v1/solver_competition/{auction_id}")) |
Regardless, I don't find the previous kind of tests still apply here.
For example, axum leverages DeserializeOwned to extract elements off of the paths, so what you should test first is the respective type's implementation of DeserializeOwned
About the errors, since trait is mocked, it simply returns what we want, and due to the handler itself being just a wrapper around the trait, we'd be just pretending to test it; if it wasn't a wrapper there would be more to it, not in this case.
So in the end, the thing that is actually useful to be tested, is the code that didn't change — the trait implementations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, makes sense. Thanks for double-checking.
Can we continue sending the same JSONs? |
This is still the case, I just asked Claude to write the description and didn't notice this. |
MartinquaXD
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks overall okay to me. Given how sensitive this change is I suggest to leave manually deploy it on mainnet staging and do some manual testing and only merge once that is okay. The reason is that if we just merge it now chances are subtle breaking changes will be overlooked and this gets rolled out to prod on Tuesday anyway.
| .and(instrumented) | ||
| .recover(handle_rejection) | ||
| .with(cors) | ||
| .with(warp::log::log(log_prefix)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The functionality of this layer was dropped. This produced logs like:
2026-02-05T13:30:38.793Z INFO http_request{request_id=d756c2c9564e3e12aabd68cac8987945}: orderbook::api::request_summary: - "POST /api/v1/quote HTTP/1.1" 200 "-" "Mozilla/5.0 (X11; Linux i686; rv:142.0) Gecko/20100101 Firefox/142.0" 4.244271624s trace_id=5fda9db1c7c2c3dc96dc4eb82da62473
Where the now lost bit is: - "POST /api/v1/quote HTTP/1.1" 200 "-" "Mozilla/5.0 (X11; Linux i686; rv:142.0) Gecko/20100101 Firefox/142.0" 4.244271624s so a log like this would probably have to be emitted from the new metrics adapter thingy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've addressed this but not sure if it is what you're expecting. With the new logging it's not clear what would be the most useful message as we can push information to the fields that will be parsed as JSON
Here you can see the log line and respective JSON

I'll be the first one to admit that log line is useless, but I'm not sure about duplicating parseable information in the log line either. I can put the timing in the log line but without the path, it's once again, not very helpful.
To make the timing useful, I can put the path, but then the method is also helpful to distinguish endpoints; if I add the method the status will also be helpful to distinguish successes from failures. So, in the end, the only thing I don't think is needed right in the log line is the user agent 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having only request_summary as the message string and all the other information as values on the log line is fine. 👌
| pub error_type: Cow<'static, str>, | ||
| pub description: Cow<'static, str>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this change required? Can we continue using <'a>?
| .requests_rejected | ||
| .with_label_values(&[response.status().as_str()]) | ||
| .inc(); | ||
| metrics.reset_requests_rejected(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it missing now?
| metrics.reset_requests_rejected(); | |
| metrics.reset_requests_complete(); | |
| metrics.reset_requests_rejected(); |
| ensure!( | ||
| self.data.order_uids.len() < ORDER_UID_LIMIT, | ||
| "order uids are limited to {ORDER_UID_LIMIT}" | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This duplicates the logic from here:
https://github.com/cowprotocol/services/pull/4080/changes#diff-e2c680b47c3828e14dc37fc519c878b8a59d477050d1e0bba26edcdef21e01fbR15-R20
|
I've dismissed the CodeQL as I've placed protections to ensure it doesn't happen for the current code |


Description
This PR migrates the orderbook API from Warp to Axum. The migration modernizes our web framework while maintaining API
compatibility, with improved code organization and reduced complexity (~200 lines removed).
Changes
Breaking Changes:
How to test
Existing tests + staging
I deployed these changes to base staging to ensure metrics were working, here's the graph
Follow up:

Unmarked spots are running
main, the wrong image was labeling endpoints with unknown because it was a fallback.That code has been removed and replaced with the current metric labeling scheme.
That scheme can be improved (read, made less verbose and error prone while being more general) but we need to change the metrics label, which I didn't do to minimize changes.