- ID
- 28b2b5c6-d81d-4183-b721-1749fcb7d6b0
BE-7208 Revisit remaining routes with trailing slashes after client migration
- source :: https://hungryroot.atlassian.net/browse/BE-7208
- tags :: Hungryroot API Project
- epic :: EP-411 Eng Parking Lot
- sprint :: HR BE Sprint 107 HR BE Sprint 108
- status :: Status - To Do
- priority :: Priority - Medium
- points :: 1
Description
Context
Follow-up to BE-1827 Remove non-slash route versions of V2 endpoints
BE-1827 added conditional trailing slashes to our endpoints and began the
process of removing the non-slash route variants. Once FE and Mobile clients
have completed their migration to the trailing-slash versions, we need to
revisit the remaining routes.
Scope
- Audit all remaining endpoints that still support the non-trailing-slash variant.
- Confirm with FE and Mobile teams that client migration is complete for each route.
- Remove the non-trailing-slash route variants for any endpoints where clients have fully migrated.
- Coordinate with relevant teams to handle any stragglers.
Acceptance Criteria
- All remaining non-trailing-slash route variants are identified.
- Routes where client migration is confirmed complete have their non-slash variants removed.
- Any routes that cannot yet be removed are documented with the reason and owning team.
Composer summary
Source files:
- ~app/rest/urls.py~ — route registrations
- ~app/tests/rest/test_urls.py~ — allowlist enforcement (lines 60–68)
View Routes (7 remaining)
These are registered without trailing slash via ~HRRestRouter.register_view~.
Each auto-generates both a ~/<route>/~ (slash) and ~/<route>~ (no-slash) variant.
| Route name | URL pattern | Line | Status |
|----------------------------------------------------------------------+---------------------------------------------------------------------+-------+--------|
| app-rest-customer-detail-customer-id | customers/<str:customer_id> | 188 | ? |
| app-rest-bundle-detail-customer-id-bundle-id | customers/<str:customer_id>/cart/bundles/<int:bundle_id> | 219 | ? |
| app-rest-bundle-product-customer-id-bundle-id-product-id | customers/<str:customer_id>/cart/bundles/<int:bundle_id>/products/<int:product_id> | 224 | ? |
| app-rest-customer-order-detail-customer-id-pk | customers/<str:customer_id>/orders/<int:pk> (nested router) | 304 | ? |
| app-rest-pairing-swaps-customer-id-pk | customers/<str:customer_id>/pairings/<int:pk>/swaps | 313 | ? |
| app-rest-customer-product-rating-detail-customer-id-product-rating-id | customers/<str:customer_id>/product_ratings/<int:pk> | 343 | ? |
| app-rest-pairing-detail-pk | pairings/<int:pk> | 431 | ? |
Viewset Routes (optional slash via ~/?~ regex)
Main router initialized with ~viewset_route_slash_required=False~ (~urls.py~ line 127),
setting ~trailing_slash = "/?~" on all registered viewsets. These accept both ~/route/~ and ~/route~.
| Viewset basename | URL prefix | Line | Status |
|---------------------------------------+-------------------------------------+------+--------|
| app-rest-address | addresses | 128 | ? |
| app-rest-customer-detail-customer-id | customers | 129 | ? |
| app-rest-checkout | customers/<str:customer_id>/checkout| 171 | ? |
| app-rest-gift | gifts | 408 | ? |
| app-rest-order | orders | 417 | ? |
| app-rest-promo-code-detail | promo | 457 | ? |
To remove viewset non-slash support: flip ~viewset_route_slash_required=False~ → ~True~ on
main router once all viewsets confirmed migrated.
Removal Checklist
For each view route confirmed migrated by FE + Mobile:
1. Add trailing slash to route string in ~app/rest/urls.py~
2. Remove route name from allowlist in ~app/tests/rest/test_urls.py~ (lines 60–68)
For viewsets (once all confirmed):
1. Change ~HRRestRouter(viewset_route_slash_required=False)~ → ~HRRestRouter()~ in ~urls.py~ line 127
Migration Status
| Route | FE migrated | Mobile migrated | Safe to remove | Notes |
|-----------------------------------------------------------------------+-------------+-----------------+----------------+-------|
| customers/<str:customer_id> | ? | ? | ? | |
| customers/<str:customer_id>/cart/bundles/<int:bundle_id> | ? | ? | ? | |
| customers/<str:customer_id>/cart/bundles/<int:bundle_id>/products/... | ? | ? | ? | |
| customers/<str:customer_id>/orders/<int:pk> | ? | ? | ? | |
| customers/<str:customer_id>/pairings/<int:pk>/swaps | ? | ? | ? | |
| customers/<str:customer_id>/product_ratings/<int:pk> | ? | ? | ? | |
| pairings/<int:pk> | ? | ? | ? | |
| addresses (viewset) | ? | ? | ? | |
| customers (viewset) | ? | ? | ? | |
| customers/.../checkout (viewset) | ? | ? | ? | |
| gifts (viewset) | ? | ? | ? | |
| orders (viewset) | ? | ? | ? | |
| promo (viewset) | ? | ? | ? | |
ChatGPT summary
Context
Follow-up to BE-1827: remove non-slash route versions of v2 endpoints once FE/Mobile have migrated to trailing-slash URLs.
Source repo (audit): =hungryroot= — URL wiring via =HRRestRouter= (=app/rest/utils/routers.py=) and =hrplatform/urls/api.py=.
How non-slash variants are created
Mechanism 1: =HRRestRouter(viewset_route_slash_required=False)=
Sets DRF =SimpleRouter.trailing_slash= to ="/?"=, so every =register_viewset= on *that same router instance* exposes both =…/= and =…= for generated list/detail/action routes.
Mechanism 2: =register_view= with route not ending in =/=
Registers *two* Django =path()= entries: slash-normalized form and a second without trailing slash; the second URL name gets suffix =-noslash= (see =HRRestRouter.register_view=).
Plain =path("something/", ...)= only defines the slash form — no automatic non-slash twin.
=/api/v2/= — Viewsets with optional trailing slash (=/?=)
Only =app/rest/urls.py= uses =HRRestRouter(viewset_route_slash_required=False)= for the main v2 app REST router.
Viewsets on *that* chained router (optional slash on DRF routes):
| Basename | Prefix |
|---------------------------------------+------------|
| =app-rest-address= | =addresses= |
| =app-rest-customer-detail-customer-id= | =customers= |
| =app-rest-gift= | =gifts= |
Nested routers in the same file use new =HRRestRouter()= instances (default =viewset_route_slash_required=True=), so e.g. checkout, nested customer orders, global orders, promo viewset, etc. do *not* get =/?= from this flag.
=*leads/urls.py*= uses stock DRF =SimpleRouter()= (not =HRRestRouter=); default behavior is trailing-slash URLs only — not the =/?= pattern.
=/api/v2/= — Explicit duplicate routes (=register_view= + =-noslash=)
Routes declared *without* trailing =/= get a second pattern; reverse name ends with =-noslash=.
=app/rest/urls.py=
- =customers/<str:customer_id>= (customer detail)
- =customers/<str:customer_id>/cart/bundles/<int:bundle_id>=
- =customers/<str:customer_id>/cart/bundles/<int:bundle_id>/products/<int:product_id>=
- =customers/<str:customer_id>/pairings/<int:pk>/swaps=
- =customers/<str:customer_id>/product_ratings/<int:pk>=
- =pairings/<int:pk>= (numeric pairing detail)
- Under nested =customers/<str:customer_id>/orders/=: =<int:pk>= (customer order detail)
=pricing_service/urls.py=
- =price_options= (under =pricing_service/customers/<str:customer_id>/=)
=/api/v2/= — Other included apps
Most v2 includes use =HRRestRouter()= without =viewset_route_slash_required=False=, and =register_view= paths usually end with =/= — no DRF =/?= and no =register_view= non-slash twin unless a route omits trailing =/=.
Inconsistency note
=plan_recommender/urls.py= uses =path("plan_recommendations", ...)= *without* trailing slash in the pattern. That is inconsistent with most v2 routes; not a “duplicate slash variant” but worth normalizing when touching that module.
=/api/v3/=
=app/rest/v3/urls.py= builds =HRRestRouter(viewset_route_slash_required=False)=, but viewsets are attached to *nested* =HRRestRouter()= instances. In practice the top-level =False= likely does not apply to any viewset today; v3 routes are mostly =register_view(..., ".../")= with explicit slashes.
Legacy and non–v2 JSON APIs
- *Legacy =/api/= v1* — =app/urls/apiv1.py= and many =re_path(..., r".../?$", ...)= patterns accept slash or not in one regex (different from v2 =HRRestRouter= behavior).
- *Internal / web / test* — e.g. =app/urls/internal_api.py=, =app/urls/web.py=, =app/urls/test.py=, =hrplatform/urls/api.py= (heartbeat, static, etc.) use =/?$= in =re_path= where applicable — not v2 JSON APIs, but still optional-slash URLs.
Suggested next steps (ticket acceptance)
1. Confirm client usage for: (a) =addresses= / =customers= / =gifts= viewsets under v2, and (b) explicit =-noslash= paths listed above.
2. Removal: set =viewset_route_slash_required= to default =True= on the main v2 router when safe; normalize every =register_view= route to end with =/=; update clients/tests that reverse =*-noslash= names.
3. Document stragglers with owning team if telemetry still shows non-slash hits.
Reference — key implementation
#+begin_src python
# app/rest/utils/routers.py — SLASH_ABSENT_ROUTE_NAME_SUFFIX = "-noslash"
# viewset_route_slash_required=False → self.trailing_slash = "/?"
# register_view: if route lacks trailing /, append second path(no_slash_route, ...)
#+end_src