Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8b99e9c
test: a role-gated v7 endpoint must enforce its roles when a path par…
hongwei1 Oct 7, 2026
462d649
refactor: let a ResourceDoc carry the route it is served by, and sele…
hongwei1 Oct 7, 2026
35769b3
refactor: bind every v7.0.0 ResourceDoc to its route
hongwei1 Oct 7, 2026
99b47f5
docs: describe binding a ResourceDoc to its route
hongwei1 Oct 7, 2026
058ebbd
test: start TrafficSourcesEndpointTest from an empty traffic record
hongwei1 Oct 7, 2026
a498ab9
fix: keep an empty path segment when reading the path parameters of a…
hongwei1 Oct 7, 2026
082ddb4
test: check the doc binding in the order the middleware selects from
hongwei1 Oct 7, 2026
464109a
perf: select the doc by route from an index and pass on requests no r…
hongwei1 Oct 7, 2026
b112b04
refactor: derive the order docs are selected in from the route chain
hongwei1 Oct 7, 2026
98bc731
refactor: bind every v6.0.0 ResourceDoc to its route
hongwei1 Oct 7, 2026
86977f4
refactor: bind every v5.1.0 ResourceDoc to its route
hongwei1 Oct 7, 2026
b337926
refactor: bind every v5.0.0 ResourceDoc to its route
hongwei1 Oct 7, 2026
0ffab86
refactor: bind every v4.0.0 ResourceDoc to its route
hongwei1 Oct 7, 2026
cd4eb11
refactor: bind every v3.1.0 ResourceDoc to its route
hongwei1 Oct 7, 2026
ce5a375
refactor: bind every v3.0.0 ResourceDoc to its route
hongwei1 Oct 7, 2026
26017fa
refactor: bind every v1.2.1 to v2.2.0 ResourceDoc to its route
hongwei1 Oct 7, 2026
0c1055a
refactor: bind every Berlin Group v1.3 and v2 ResourceDoc to its route
hongwei1 Oct 7, 2026
3ad5274
refactor: bind every UK Open Banking v2.0.0, v3.1.0 and v4.0.1 Resour…
hongwei1 Oct 7, 2026
0dee96e
test: check every ResourceDoc catalog is bound to its routes, and sel…
hongwei1 Oct 7, 2026
2282898
chore: record the new v4.0.0 createTransactionRequestAccount URL in t…
hongwei1 Oct 7, 2026
8cf5963
refactor: remove the template matching that chose a ResourceDoc
hongwei1 Oct 7, 2026
72fff21
docs: describe route-bound ResourceDocs for every version
hongwei1 Oct 7, 2026
8553413
fix: document the query segment of the v2.0.0 search endpoints
hongwei1 Oct 7, 2026
338a01c
fix: authenticate a dynamic entity write before its transaction opens
hongwei1 Oct 7, 2026
02c9728
refactor: remove the once-per-request caller holder
hongwei1 Oct 7, 2026
4fc8760
docs: drop references to the removed template matching
hongwei1 Oct 7, 2026
f6b702c
refactor: rename the http4sbridge test package to http4sserver
hongwei1 Oct 7, 2026
5e403a1
docs: bring the status doc and bridge notes in line with route-bound …
hongwei1 Oct 7, 2026
248f0e2
docs: remove the comments that still describe the Lift bridge as a li…
hongwei1 Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/scripts/test_speed_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
HTTP4S_INTEGRATION_SUITES = {
"code.api.v7_0_0.Http4s700RoutesTest",
"code.api.v7_0_0.Http4s700TransactionTest",
"code.api.http4sbridge.Http4sServerIntegrationTest",
"code.api.http4sserver.Http4sServerIntegrationTest",
"code.api.v5_0_0.Http4s500SystemViewsTest",
}

Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/build_container.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ jobs:
# Shard 3 ~155s v6_0_0 only (isolated after v2_x moved to shard 7)
# Shard 4 ~183s v5_1_0 v5_0_0 v3_0_0
# Shard 5 ~193s ResourceDocs v3_1_0 v1_4_0 v1_3_0
# Shard 6 ~168s v7_0_0 http4sbridge UKOpenBanking
# Shard 6 ~168s v7_0_0 http4sserver UKOpenBanking
# Shard 7 ~280s model + views + customer + util + berlin + v2_x
# Shard 8 ~240s connector + auth + login + mgmt + metrics + catch-all
# Shard 9 ~110s v4_0_0 Dynamic* (6 heavy test classes)
Expand Down Expand Up @@ -170,10 +170,10 @@ jobs:
code.api.v1_4_0
code.api.v1_3_0
- shard: 6
name: "v7 + http4sbridge + UKOpenBanking"
name: "v7 + http4sserver + UKOpenBanking"
test_filter: >-
code.api.v7_0_0
code.api.http4sbridge
code.api.http4sserver
code.api.UKOpenBanking
- shard: 7
name: "model + views + customer + util + small data + berlin + v2_x"
Expand Down Expand Up @@ -382,7 +382,7 @@ jobs:
SHARD3="code.api.v6_0_0"
SHARD4="code.api.v5_1_0 code.api.v5_0_0 code.api.v3_0_0"
SHARD5="code.api.ResourceDocs1_4_0 code.api.v3_1_0 code.api.v1_4_0 code.api.v1_3_0"
SHARD6="code.api.v7_0_0 code.api.http4sbridge code.api.UKOpenBanking"
SHARD6="code.api.v7_0_0 code.api.http4sserver code.api.UKOpenBanking"
SHARD7="code.model code.views code.customer code.usercustomerlinks \
code.api.util code.errormessages code.atms code.branches \
code.products code.crm code.accountHolder code.api.berlin \
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/build_pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ jobs:
# Shard 3 ~250s v6_0_0 (split from v2_x; v2_x moved to shard 7)
# Shard 4 ~232s v5_1_0 + v5_0_0 + v3_0_0
# Shard 5 ~252s ResourceDocs + v3_1_0 + v1_4_0 + v1_3_0
# Shard 6 ~200s v7_0_0 + http4sbridge + UKOpenBanking
# Shard 6 ~200s v7_0_0 + http4sserver + UKOpenBanking
# Shard 7 ~280s model + views + customer + util + berlin + small data + v2_x
# Shard 8 ~240s connector + auth + login + mgmt + metrics + catch-all
# Shard 9 ~300s v4_0_0 Dynamic* (6 classes: 9 400+ lines each)
Expand Down Expand Up @@ -164,10 +164,10 @@ jobs:
code.api.v1_4_0
code.api.v1_3_0
- shard: 6
name: "v7 + http4sbridge + UKOpenBanking"
name: "v7 + http4sserver + UKOpenBanking"
test_filter: >-
code.api.v7_0_0
code.api.http4sbridge
code.api.http4sserver
code.api.UKOpenBanking
- shard: 7
name: "model + views + customer + util + small data + berlin + v2_x"
Expand Down Expand Up @@ -387,7 +387,7 @@ jobs:
SHARD3="code.api.v6_0_0"
SHARD4="code.api.v5_1_0 code.api.v5_0_0 code.api.v3_0_0"
SHARD5="code.api.ResourceDocs1_4_0 code.api.v3_1_0 code.api.v1_4_0 code.api.v1_3_0"
SHARD6="code.api.v7_0_0 code.api.http4sbridge code.api.UKOpenBanking"
SHARD6="code.api.v7_0_0 code.api.http4sserver code.api.UKOpenBanking"
SHARD7="code.model code.views code.customer code.usercustomerlinks \
code.api.util code.errormessages code.atms code.branches \
code.products code.crm code.accountHolder code.api.berlin \
Expand Down
24 changes: 14 additions & 10 deletions CLAUDE.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions DYNAMIC_ENTITY_SPACE_MODEL_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,11 @@ writing. `/obp/dynamic-entity/…` keeps serving unchanged throughout; the two r
by every endpoint in OBP, so relaxing it there would let `/banks/SYS/accounts` past bank validation
for endpoints that genuinely need a bank, and they would fail further in with something worse than
a 404. Instead the Dynamic Entity ResourceDocs declare their template with `SPACE_ID`, a
non-standard all-caps variable the matcher treats as a wildcard and the middleware skips — the
non-standard all-caps variable the middleware does not read — the
documented bypass in CLAUDE.md, already used by `FIREHOSE_BANK_ID` and `NEW_ACCOUNT_ID`. The
handler then calls the resolver itself, in place of today's `bankCheck`
(`Http4sDynamicEntity.scala:173`). `SPACE_ID` is not in `ResourceDocMatcher.literalAllCapsSegments`,
so nothing else has to change.
(`Http4sDynamicEntity.scala:173`). The middleware only reads `BANK_ID`, `ACCOUNT_ID`, `VIEW_ID` and
`COUNTERPARTY_ID` from a template, so nothing else has to change.

The served URL is unaffected: a caller still writes `/banks/obp1/...` or `/banks/SYS/...`; only the
doc's template variable is named differently, which is what the middleware matches on.
Expand Down
8 changes: 4 additions & 4 deletions OPEN_CORRIDOR_INTERFACE_C_PUBLISH_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -250,10 +250,10 @@ Rename inventory (OBP-API):
- prop name `transactionRequests_challenge_threshold_OPEN_CORRIDOR` →
`..._OPEN_CORRIDOR_PROMISE`;
- `OpenCorridorProcessor.scala:42`'s type constant;
- **add both new names to `literalAllCapsSegments` in `Http4sSupport.scala`**
so the ResourceDoc matcher treats them as literal path segments, not
wildcards. (Latent quirk found 2026-07-18: `OPEN_CORRIDOR` itself is
missing from that set today — the rename must not reproduce that.)
- no matcher list to update: a ResourceDoc is selected by the route that
serves the request, so a new transaction-request type needs only its route
pattern and its doc. (The old `literalAllCapsSegments` set, which `OPEN_CORRIDOR`
was missing from, was removed when docs were bound to their routes.)
- tests + any dev/test data rows carrying the old type string.

Bank Node side: its submit-TR client URL and any internal type strings
Expand Down
34 changes: 18 additions & 16 deletions docs/resource_doc_and_endpoint_consistency_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@ endpoint has one.
endpoint discovery and the Glossary's endpoint links are all generated from ResourceDocs.

**Building the routes.** In every versioned API group, the routes are built from the ResourceDocs
themselves: `allRoutes` is the list of each doc's `http4sPartialFunction`, ordered by the number of
path segments (for example `Http4s700.scala`, `val allRoutes`). So in those groups a route cannot
exist without a ResourceDoc. The same construction is used in 11 route groups.
themselves: each version lists its routes in `routesInOrder` (for example `Http4s700.scala`, which sorts
them by the number of path segments; the other versions keep the order they were written in), and
`ResourceDocMatcher.orderByRoutes` puts the docs in that same order. A doc whose route is missing from
the list is an error at startup, so in those groups a route cannot exist without a ResourceDoc.
Every versioned group is built this way: v1.2.1 to v7.0.0, Berlin Group v1.3 and v2, and UK Open
Banking v2.0.0, v3.1.0 and v4.0.1.

**Checking every request, in `ResourceDocMiddleware`.** Each versioned group is wrapped in
`ResourceDocMiddleware.apply(resourceDocs)` (`obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala`).
For each request it finds the matching ResourceDoc and then, in this order
`ResourceDocMiddleware.apply(orderedResourceDocs, ...)` (`obp-api/src/main/scala/code/api/util/http4s/ResourceDocMiddleware.scala`).
For each request it asks the group's routes which one serves it (`ResourceDocMatcher.selectByRoute`),
takes that route's ResourceDoc, and then, in this order
(`validateOnly`, which follows the order Lift used):

1. rejects duplicated query parameters;
Expand Down Expand Up @@ -55,11 +59,12 @@ Every group below builds its routes from its ResourceDocs and is wrapped by
- Berlin Group v1.3 (and its alias path) and v2;
- UK Open Banking v2.0, v3.1 and v4.0.1.

One narrow exception inside these groups: when no ResourceDoc matches (for example a URL with an
empty segment such as `/banks//accounts`), the middleware still lets the group's routes try the
request, after resolving the caller but without the doc-based checks. That branch exists so a
malformed URL gets 403 or 404 rather than a misleading 401. It is documented in `CLAUDE.md`
("Empty path segments").
There is no exception inside these groups. A request that none of a group's routes serves is passed
on to the next link of the chain at once, without resolving the caller. A request a route does serve
is validated under that route's ResourceDoc, including a URL with an empty segment such as
`/banks//accounts`: the empty segment is read as an empty `BANK_ID`, so the Roles and the bank
lookup are checked against it instead of the handler running unvalidated. It is documented in
`CLAUDE.md` ("Empty path segments").

## 4. Where it does not hold

Expand Down Expand Up @@ -136,9 +141,9 @@ each of which changes what callers receive:
2. The docs are declared once, at v1.4.0, but the routes answer under every version prefix, and
the output depends on the prefix: v4.0.0 and later get the newer shape, v6.0.0 also adds the
technology field (`Http4sResourceDocs.scala`, `includeTechnologyForPrefix` and the
`isVersion4OrHigher` choice in `routes`). The middleware matches a doc by the version in the
path, so it would need a copy of each doc in every version group, or a matcher that ignores
versions, and either change reaches beyond these routes.
`isVersion4OrHigher` choice in `routes`). The middleware selects a doc from the routes of the
version group in the path, so it would need a copy of each doc in every version group, or a
selection that ignores versions, and either change reaches beyond these routes.
3. They are open by default and need a Role only when `resource_docs_requires_role=true`, checked
inline (`withOptionalRoleCheck`). A doc can declare a Role conditionally, but the choice is
fixed when the docs are built at start-up (see the shard 10 note in `CLAUDE.md`).
Expand All @@ -156,6 +161,3 @@ section 13).
- `docs/telemetry_conventions.md`, section 13: which of these routes Telemetry does not time.
- `CLAUDE.md`: the migration rules (ResourceDoc registration order, the middleware's handling of
`BANK_ID`, `ACCOUNT_ID`, `VIEW_ID`, `COUNTERPARTY_ID`, and the gotchas).
- Stale comment: `Http4sDynamicEndpoint.scala`'s header still says an unmatched request falls
through to "the Lift bridge"; since the bridge was removed it reaches `notFoundCatchAll` (JSON
404).
2 changes: 1 addition & 1 deletion docs/testing/EXTRA_TESTS_TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Tracks dependency bumps where compile + the standard 4-suite smoke test passed,
Test suites currently used as the smoke gate:
- `code.api.v7_0_0.Http4s700RoutesTest`
- `code.api.v7_0_0.Http4s700TransactionTest`
- `code.api.http4sbridge.Http4sServerIntegrationTest`
- `code.api.http4sserver.Http4sServerIntegrationTest`

Test DB is H2; many integrations are stubbed or absent.

Expand Down
2 changes: 1 addition & 1 deletion obp-api/src/main/scala/code/api/DirectLoginRoutes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ import scala.concurrent.Future
* paths are served by per-version files (e.g. `Http4s600.directLoginEndpoint`).
*
* Adding this route lets us retire `LiftRules.statelessDispatch.append(DirectLogin)`
* in `Boot.scala` — it was the last consumer of the bare path on the Lift bridge.
* in `Boot.scala` — it was the last Lift registration serving the bare path.
*/
object DirectLoginRoutes {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ package code.api.UKOpenBanking.v2_0_0

import cats.data.{Kleisli, OptionT}
import cats.effect._
import code.api.util.APIUtil.ResourceDoc
import code.api.util.http4s.ResourceDocMiddleware
import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, ResourceDoc}
import code.api.util.http4s.{ResourceDocMatcher, ResourceDocMiddleware}
import code.api.util.http4s.IdempotencyMiddleware
import code.util.Helper.MdcLoggable
import com.openbankproject.commons.util.ApiVersion
Expand All @@ -46,7 +46,7 @@ import scala.collection.mutable.ArrayBuffer
* account-scoped ones (/accounts/ID/balances, /accounts/ID/transactions) — are
* migrated in Http4sUKOBv200AIS. The Lift ScannedApis aggregator
* (OBP_UKOpenBanking_200) registers `routes = Nil`, so no UK v2.0 path is served
* by Lift — nothing falls through to the Lift bridge.
* by Lift.
*/
object Http4sUKOBv200 extends MdcLoggable {

Expand All @@ -57,9 +57,12 @@ object Http4sUKOBv200 extends MdcLoggable {
val resourceDocs: ArrayBuffer[ResourceDoc] =
Http4sUKOBv200AIS.resourceDocs

val allRoutes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req =>
Http4sUKOBv200AIS.routes(req)
}
lazy val routesInOrder: List[Http4sHandler] =
Http4sUKOBv200AIS.routesInOrder

val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(resourceDocs)(IdempotencyMiddleware(allRoutes))
lazy val orderedResourceDocs: ArrayBuffer[ResourceDoc] = ResourceDocMatcher.orderByRoutes(resourceDocs, routesInOrder)

lazy val allRoutes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder)

lazy val wrappedRoutes: HttpRoutes[IO] = ResourceDocMiddleware.apply(orderedResourceDocs, routes => IdempotencyMiddleware(routes))
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import cats.data.{Kleisli, OptionT}
import cats.effect.IO
import code.api.APIFailureNewStyle
import code.api.ResourceDocs1_4_0.SwaggerDefinitionsJSON
import code.api.util.APIUtil.{HTTPParam, EmptyBody, ResourceDoc, createQueriesByHttpParams, fullBoxOrException, unboxFull}
import code.api.util.APIUtil.{Http4sHandler, Http4sRoute, HTTPParam, EmptyBody, ResourceDoc, createQueriesByHttpParams, fullBoxOrException, unboxFull}
import code.api.util.ApiTag._
import code.api.util.CustomJsonFormats
import code.api.util.ErrorMessages.{AuthenticatedUserIsRequired, UnknownError}
Expand Down Expand Up @@ -74,7 +74,7 @@ object Http4sUKOBv200AIS extends MdcLoggable {
val ukV20Prefix = Root / ApiVersion.ukOpenBankingV20.urlPrefix / ApiVersion.ukOpenBankingV20.apiShortVersion

// GET /accounts — list all private accounts of the logged-in user
lazy val getAccountList: HttpRoutes[IO] = HttpRoutes.of[IO] {
lazy val getAccountList: Http4sRoute = Http4sRoute {
case req @ GET -> `ukV20Prefix` / "accounts" =>
EndpointHelpers.withUser(req) { (u, cc) =>
val callContext = Some(cc)
Expand All @@ -99,7 +99,7 @@ object Http4sUKOBv200AIS extends MdcLoggable {
)

// GET /accounts/{accountId} — single account
lazy val getAccount: HttpRoutes[IO] = HttpRoutes.of[IO] {
lazy val getAccount: Http4sRoute = Http4sRoute {
case req @ GET -> `ukV20Prefix` / "accounts" / accountId =>
EndpointHelpers.withUser(req) { (u, cc) =>
val callContext = Some(cc)
Expand All @@ -124,7 +124,7 @@ object Http4sUKOBv200AIS extends MdcLoggable {
)

// GET /balances — bulk balances for all private accounts
lazy val getBalances: HttpRoutes[IO] = HttpRoutes.of[IO] {
lazy val getBalances: Http4sRoute = Http4sRoute {
case req @ GET -> `ukV20Prefix` / "balances" =>
EndpointHelpers.withUser(req) { (u, cc) =>
val callContext = Some(cc)
Expand All @@ -149,7 +149,7 @@ object Http4sUKOBv200AIS extends MdcLoggable {
)

// GET /accounts/{accountId}/balances — account-level balances
lazy val getAccountBalances: HttpRoutes[IO] = HttpRoutes.of[IO] {
lazy val getAccountBalances: Http4sRoute = Http4sRoute {
case req @ GET -> `ukV20Prefix` / "accounts" / accountIdStr / "balances" =>
EndpointHelpers.withUser(req) { (u, cc) =>
for {
Expand All @@ -176,7 +176,7 @@ object Http4sUKOBv200AIS extends MdcLoggable {
)

// GET /accounts/{accountId}/transactions — account-level transactions
lazy val getAccountTransactions: HttpRoutes[IO] = HttpRoutes.of[IO] {
lazy val getAccountTransactions: Http4sRoute = Http4sRoute {
case req @ GET -> `ukV20Prefix` / "accounts" / accountIdStr / "transactions" =>
EndpointHelpers.withUser(req) { (u, cc) =>
for {
Expand Down Expand Up @@ -206,11 +206,13 @@ object Http4sUKOBv200AIS extends MdcLoggable {
http4sPartialFunction = Some(getAccountTransactions)
)

val routes: HttpRoutes[IO] = Kleisli[HttpF, Request[IO], Response[IO]] { req =>
getAccountList(req)
.orElse(getAccount(req))
.orElse(getAccountBalances(req))
.orElse(getAccountTransactions(req))
.orElse(getBalances(req))
}
lazy val routesInOrder: List[Http4sHandler] = List(
getAccountList,
getAccount,
getAccountBalances,
getAccountTransactions,
getBalances
)

lazy val routes: HttpRoutes[IO] = Http4sRoute.chain(routesInOrder)
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import scala.collection.mutable.ArrayBuffer
* This stub is retained for ScannedApis registration (class-path scanning) and
* so that external callers (APIUtil, SwaggerJSONFactory) that access
* OBP_UKOpenBanking_200.apiVersion / .allResourceDocs continue to compile.
* Routes are served by Http4sUKOBv200.wrappedRoutes in Http4sApp (ahead of the Lift bridge).
* Routes are served by Http4sUKOBv200.wrappedRoutes in Http4sApp.
*/
object OBP_UKOpenBanking_200 extends OBPRestHelper with MdcLoggable with ScannedApis {

Expand Down
Loading
Loading