Org Web Adapter

hungryroot/jira/be_7208_revisit_remaining_routes_with_trailing_slashes_after_client_migration.org

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