Skip to content

Refactor/route bound resource doc - #2937

Open
hongwei1 wants to merge 29 commits into
OpenBankProject:developfrom
hongwei1:refactor/route-bound-resource-doc
Open

hongwei1 wants to merge 29 commits into
OpenBankProject:developfrom
hongwei1:refactor/route-bound-resource-doc

Conversation

@hongwei1

@hongwei1 hongwei1 commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

No description provided.

@hongwei1
hongwei1 force-pushed the refactor/route-bound-resource-doc branch 2 times, most recently from 2a22501 to 9d18edd Compare October 7, 2026 14:47
…ameter is empty

GET /banks/BANK_ID/api-products/API_PRODUCT_CODE/subscriptions requires
canGetApiProductSubscriptionAtOneBank, which only ResourceDocMiddleware
enforces. With an empty API_PRODUCT_CODE segment the http4s route still
matches but the middleware finds no ResourceDoc, so the handler runs
unvalidated and answers 500 (Bank not found in CallContext) instead of
403 for a caller without the role.

The empty-segment scenario fails until the middleware selects the
ResourceDoc from the route that serves the request.
…ct the doc by route

ResourceDocMiddleware finds the ResourceDoc of a request by matching the URL
against the doc templates, separately from the http4s route that runs the
handler. The two can disagree, and then the request is validated under one
endpoint's roles, enable/disable switch and operationId while another
endpoint's handler runs.

Add Http4sRoute, which keeps the pattern match of an endpoint so that it can
be asked whether it serves a request, and ResourceDocMatcher.selectByRoute,
which returns the doc of the first route that does. When several docs share
one route, their templates tell them apart. ResourceDocMiddleware tries the
route-bound docs first and runs only the selected route, wrapped in the new
`wrap` argument; docs without a route are still found by template, so
versions that have not been converted behave as before. An implicit
conversion from HttpRoutes[IO] keeps the existing
`http4sPartialFunction = Some(routes)` sites compiling.
Declare each v7.0.0 endpoint as an Http4sRoute instead of HttpRoutes.of, so
the middleware can ask it whether it serves a request. The same ordered doc
list now drives both selecting the doc and running the routes, and
ResourceDocMiddleware runs only the selected route, wrapped in the
idempotency middleware. The v7.0.0 to v6.0.0 bridge asks the routes whether
any of them serves a path instead of matching the URL against templates.

A request whose path parameter is empty (/banks/B/api-products//subscriptions)
is now served by the route that matches it and validated under that route's
doc, so its roles are enforced instead of the handler running unvalidated.

ResourceDocRouteBindingTest checks that every v7.0.0 doc is bound to a route
that serves its own URL and that selecting by route returns that doc.
Explain how an endpoint is declared as an Http4sRoute so the middleware
selects its ResourceDoc by route, and how that differs from the template
matching that versions not yet converted still use.
GET /obp/v7.0.0/management/traffic/top-callers lists only the 50 busiest
caller and endpoint pairs of the window, and the window is the current
minute of the JVM. Each pair the test looks for has a single request, so
when other suites had made more than 50 pairs in the same minute the pair
was cut off and the test failed. It passed alone and failed after
AuthSweepTest in one JVM.

Clear the record at the start of the scenario so it only sees its own
requests.
… route-selected doc

A route can serve a path with an empty segment, such as
/banks/B/api-products//subscriptions, and the doc selected for it is the
route's. extractPathParams dropped empty segments before comparing the path
with the template, so the lengths differed and it returned no parameters.
BANK_ID, ACCOUNT_ID and VIEW_ID were then never validated, and the role check
asked for the role at bank "": a caller holding the role at bank B was refused
with 403, and a handler that needs the bank or account answered 500.

Read the parameters by position with the empty segment kept when that matches
the template, and fall back to the previous reading otherwise, so docs that
are still matched by template behave as before.
ResourceDocRouteBindingTest sent each doc's URL through selectByRoute with the
docs in registration order, but the v7.0.0 middleware selects from the docs
sorted by segment count, the order its routes are tried in. A doc shadowed
by another one in that order would have passed the test and been validated
and run as the wrong endpoint in production. Use the ordered list.
…oute serves

selectByRoute asked every route-bound doc on every request. The v7.0.0
middleware sits ahead of the older versions in the chain, so a v4 or v3
request paid for about 120 pattern matches in v7.0.0 first, and the docs that
share a route were looked up again with a pass over all docs each time.

Build an index of the route-bound docs by verb and API version when the
middleware is built, with the docs that share a route worked out once, and ask
only the routes of the request's verb and version. The v7.0.0 to v6.0.0 bridge
uses the same index.

When every doc of a group carries its route, a request no route serves is
passed on at once, before the CallContext is built: nothing else in the group
could serve it, so resolving the caller and running all the routes again
could only find nothing.

Also report a route that is null (an endpoint val declared after the
resourceDocs += line that uses it) by the endpoint's name when the middleware
is built, and say why a doc that shares a route with others falls back to the
first of them when no template matches.
Add Http4sRoute.chain, which tries handlers in the order given, and
ResourceDocMatcher.orderByRoutes, which puts a version's docs in the order its
routes are tried. Docs that share a route stay together and an alias of a route
counts as the same route. A doc whose route is missing from the chain is an
error naming the doc, unless the route is listed as deliberately run outside
the middleware; a doc with no route documents an endpoint served elsewhere and
is left out. The route index is bucketed by apiShortVersion so Berlin Group
and UK Open Banking versions are found as well.
Declare each endpoint as an Http4sRoute and derive the route chain from one
list, so the middleware selects the doc of the route that serves a request and
runs only that route. The path-rewriting bridge asks the routes whether the
version serves a path instead of matching the URL against doc templates.
Declare each endpoint as an Http4sRoute and derive the route chain from one
list, so the middleware selects the doc of the route that serves a request and
runs only that route. The path-rewriting bridge asks the routes whether the
version serves a path instead of matching the URL against doc templates.
Declare each endpoint as an Http4sRoute and derive the route chain from one
list, so the middleware selects the doc of the route that serves a request and
runs only that route. The path-rewriting bridge asks the routes whether the
version serves a path instead of matching the URL against doc templates.
Declare each endpoint as an Http4sRoute and derive the route chain from one
list, so the middleware selects the doc of the route that serves a request and
runs only that route. The path-rewriting bridge asks the routes whether the
version serves a path instead of matching the URL against doc templates.

The createTransactionRequestAccount doc now documents the ACCOUNT URL, as the Lift
baseline does; a request for a type that has no doc of its own is validated under
it, the first doc of the shared route.
Declare each endpoint as an Http4sRoute and derive the route chain from one
list, so the middleware selects the doc of the route that serves a request and
runs only that route. The path-rewriting bridge asks the routes whether the
version serves a path instead of matching the URL against doc templates.

createConsent is shared by the EMAIL, SMS and IMPLICIT docs. The message-docs
Swagger doc has no route here (Http4sResourceDocs serves it) and stays out of the
middleware's docs.
Declare each endpoint as an Http4sRoute and derive the route chain from one
list, so the middleware selects the doc of the route that serves a request and
runs only that route. The path-rewriting bridge asks the routes whether the
version serves a path instead of matching the URL against doc templates.
Declare each endpoint of v1.2.1, v1.3.0, v1.4.0, v2.0.0, v2.1.0 and v2.2.0 as an
Http4sRoute and derive the route chain from one list. bankById keeps running
before the middleware so an unknown bank answers 400, not 464, and is listed as
outside the chain.
Each group lists its routes in order and the aggregator concatenates them, so
the middleware selects the doc of the route that serves a request. A signing
basket's status and authorisations no longer fall to getPaymentInformation,
whose template is made only of placeholders. The v2 routes are declared as
lazy vals because their docs are registered before them.
…ceDoc to its route

Each group lists its routes in order and the aggregator concatenates them, so
the middleware selects the doc of the route that serves a request.
…ect real URLs

ResourceDocRouteBindingTest covers all 18 catalogs (v1.2.1 to v7.0.0, Berlin
Group v1.3 and v2, UK Open Banking v2.0.0, v3.1.0, v4.0.1): each doc's own route
must serve its own URL and selection must return that doc. ResourceDocRouteOrderTest
covers orderByRoutes and Http4sRoute.chain. ResourceDocRealCatalogSelectionTest
selects real URLs that used to be given another endpoint's doc: the v4.0.0 and
v6.0.0 transaction-request types, the types v7.0.0 has no route for, and the
Berlin Group signing-basket status and authorisations.
…he parity allowlist

The doc now documents the ACCOUNT URL of the Lift baseline; only the VIEW_ID to
GRANT_VIEW_ID difference remains, and its digest is recomputed with
allowlist_helper.py.
Every doc of every catalog now carries its route, so the middleware selects the
doc by asking the routes and passes on a request no route serves. Delete the
URL-template index and lookup (buildIndex, findResourceDoc), the guess that a
capitalised segment is a placeholder (isTemplateVariable, literalAllCapsSegments),
the no-doc branch of the middleware and the caller resolution it needed.

ResourceDocMiddleware.apply now returns the routes itself: it runs the route of
the selected doc, so it no longer takes the chain as a second argument. A doc
with no route, or a null one, fails when the middleware is built and names the doc.

Docs that share one route are told apart by the segments the request has in
common with each template, with the first doc as the fallback, instead of by a
list of enum values. The path parameters read from a template are the four the
middleware validates: BANK_ID, ACCOUNT_ID, VIEW_ID and COUNTERPARTY_ID.

ResourceDocSelfResolveTest is replaced by ResourceDocRouteBindingTest, which
checks the same thing through the routes. ResourceDocMatcherTest keeps the path
parameter and CallContext scenarios.
Rewrite the guidance on declaring an endpoint, the route chain, docs that share a
route and the empty path segment gotcha, now that no version matches docs by URL
template.
elasticSearchWarehouse and elasticSearchMetrics documented /search/warehouse and
/search/metrics, but their routes serve /search/warehouse/{query} and
/search/metrics/{query}: the query is a path segment, so the documented URL could
not be called. Matching docs by template hid this, because the template validated
a URL no route served. The docs now name the segment, and the parity allowlist
records the difference from the Lift baseline.
Authenticating a consent request writes: it creates the consent's user and copies
the consent's Roles onto it. Http4sDynamicEntity authenticated inside the write
transaction, so those writes were uncommitted when the handler's own checks read
them on other connections: a consent that carried the Role was refused with 403
(OBP-20006), and a personal row was owned by the consent user instead of the
human who granted the consent, so the consent could not read back what it wrote.

The first link of the version chain used to authenticate the request outside any
transaction and committed those writes as a side effect; every version now serves
only the requests its routes serve, so nothing did. Resolve the caller before the
transaction opens, as ResourceDocMiddleware does for every other endpoint.

DynamicEntityConsentUserTest covers it: two scenarios failed on the shard that
runs v6.0.0.
The holder shared a caller resolved by the first version hop that had no ResourceDoc
for a request. Every version group now passes on a request none of its routes serves
without resolving anything, so nothing fills or reads it. Update the doc comment of
resolveCallerWithoutRateLimiting, whose one user is now the dynamic entity service.
The plan documents, the AuthSweep catalog comments and the 64KB note in CLAUDE.md
still named literalAllCapsSegments and isTemplateVariable, or told the reader to add
a transaction-request type to that set. A doc is now selected by the route that serves
the request, so no list needs updating.
The Lift bridge it was named for no longer exists, and its one suite is
Http4sServerIntegrationTest. Move the package and update the CI shard filters,
shard names, the local runner, the speed report and the docs that list it.
…ResourceDocs

resource_doc_and_endpoint_consistency_status.md still said the routes are ordered by
path segment count, that the middleware matches a doc by URL, and that a request with no
matching doc is still tried against the group's routes after resolving the caller. None of
that holds now: each version lists its routes in routesInOrder, the doc is the one of the
route that serves the request, and a request no route serves is passed on at once.

CLAUDE.md said every path-rewriting bridge asks selectByRoute. Only v5.1.0, v6.0.0 and
v7.0.0 do; v5.0.0 and below test the path prefix and are chained after their own routes.
Say so, including that a disabled endpoint falls to the version below in those.

Remove the remaining references to the Lift bridge in the comments of the files this
change touched.
…ve component

Http4sLiftWebBridge is gone; a request no route serves reaches notFoundCatchAll. The
comments on the Http4sApp entry points, the resource-docs and dynamic endpoint/entity
services, the UK Open Banking and Berlin Group file headers and the v2.1.0 transaction
request docs still said routes sit ahead of, or fall through to, the bridge. Say what
happens now, and drop the status doc's note about the stale comment.

Statements that the bridge was removed are left as they are.
@hongwei1
hongwei1 force-pushed the refactor/route-bound-resource-doc branch from bbf4d4b to 248f0e2 Compare October 7, 2026 20:11
@sonarqubecloud

sonarqubecloud Bot commented Oct 7, 2026

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant