Skip to content

refactor: remove Lift Web framework — full http4s runtime + OIDC migration#2828

Merged
simonredfern merged 68 commits into
OpenBankProject:developfrom
hongwei1:refactor/RemoveLiftwebCode
Jun 4, 2026
Merged

refactor: remove Lift Web framework — full http4s runtime + OIDC migration#2828
simonredfern merged 68 commits into
OpenBankProject:developfrom
hongwei1:refactor/RemoveLiftwebCode

Conversation

@hongwei1
Copy link
Copy Markdown
Contributor

@hongwei1 hongwei1 commented Jun 2, 2026

Remove Lift Web framework — full http4s runtime

Final teardown stage of the Lift → http4s migration. The runtime Lift Web framework is now entirely removed. Endpoint migration for every API version landed in earlier PRs; this PR removes the Lift scaffolding kept alive only as a fallback (the bridge) plus the last non-endpoint Lift surfaces (OpenID Connect callbacks, Boot lifecycle, the REST-helper base, portal-form code in AuthUser, aliveCheck).

After this PR there is no net.liftweb.http anywhere in source0 references (the final comment lines in APIUtil.scala and the last live call, AuthUser.logout's S.redirectTo, were both removed). Lift libraries still used are non-web and out of scope: lift-json, lift-common (Box), lift-util, lift-mapper (ORM).

This is an in-place migration — no API version bump (versioning is tech-agnostic; framework swaps don't change signatures).

Scope: 230 files, +2,151 / −12,408 (net −10k), 67 commits.

1. Lift Web teardown

  • Http4sLiftWebBridge.scala DELETED (−583). Was the fallback handing unmatched routes to Lift dispatch. Unmatched /obp/* now terminates at notFoundCatchAll in Http4sApp (JSON 404). The chain is pure http4s.
  • Boot.scala (−248). All LiftRules.* removed: unload hooks → Runtime.getRuntime.addShutdownHook; charset/suffix/locale calculators dropped (API-only); supplementalHeaders (X-Frame-Options) → Http4sStandardHeaders; DB.buildLoanWrapper, exception handler, uriNotFound, OIDC dispatch removed.
  • OBPRestHelper.scala (−562). No longer extends Lift RestHelper. Dropped the jsonResponseBoxToJsonResponse implicit, failIfBadJSON, Lift JSON marshalling; JsonResponse is now a lightweight local carrier; localization via plain ResourceBundle.getBundle.
  • APIUtil.scala (−712). Removed ResourceDoc.partialFunction (Lift OBPEndpoint) and wrappedWithAuthCheck (auth now in http4s middleware); S.request-derived values replaced; HTTPParam re-implemented locally.
  • AuthUser.scala (−219). Portal-form _toForm / S.notice / S.redirectTo machinery purged (including the final logout override — dead code, Lift SiteMap/Menu being disabled); now a data model only. getCurrentUser returns Empty.
  • aliveCheck.scala DELETED → native http4s AliveCheckRoutes.scala (wired in Http4sApp.baseServices).
  • AccountsAPI.scala DELETED — a commented-out, non-functional Lift stub.

2. OpenID Connect → http4s

  • NEW Http4sOpenIdConnect.scala (+391). Migrates the 3 callback routes (/auth/openid-connect/callback[-1|-2], GET+POST) from Lift serve{} dispatch. Wired in Http4sApp after DirectLogin, before notFoundCatchAll, gated by openid_connect.enabled (default off).
  • Behavioural change: success now mints an OBP DirectLogin token ({"token": "..."}) instead of a Lift session redirect — stateless, suits mobile/SPA clients. Old openidconnect.scala fully commented out.
  • Hardening: provisioning (resource user / auth user / entitlements / consumer / token persistence) is wrapped in a single DB transaction (commit-together, rollback-on-error); token-exchange debug logs are scrubbed of client_secret and id/access/refresh tokens. Config caveat: openid_connect.check_session_state defaults true (fail-closed); with portal-login gone there is no server-side state to compare, so live deployments must set it false (documented in code + sample.props).
  • Known limitation: the flow runs inside IO.blocking (provider token exchange + JWT validation + DB provisioning are blocking) — preserves prior behaviour but pins a compute thread for the call's duration. Covered by Http4sOpenIdConnectRoutesTest (5 routing/failure scenarios) and Http4sOpenIdConnectSuccessTest (success path).

3. New http4s util surfaces

  • Http4sStandardHeaders.scala (NEW). Correlation-ID, no-cache headers, X-Frame-Options: DENY — extracted from the deleted bridge, applied to every response.
  • Http4sResourceDocAggregation.scala (NEW +116). Lift-free resource-doc catalog: dedup by (requestUrl, verb) keeping the newest version; replaces the old OBPAPINxx.allResourceDocs Lift-dispatch ancestry.

4. v7 duplicate-endpoint removal

19 duplicate POC endpoints removed from Http4s700.scala (−750) → cascade to v6/v4 twins. scripts/remove_duplicate_v7_endpoints.py is the removal tool.

5. CI / test infra

  • 4 → 8 shards (build_pull_request.yml): isolate v1_2_1 and v4; rebalance berlin/mgmt/metrics to light shards. A catch-all shard absorbs any package not explicitly assigned (verified at runtime: code.external, code.setup, com.openbankproject.commons.util).
  • Test invocation: mvn testmvn process-resources scalatest:testprocess-resources keeps the test props on the classpath, so a red test actually fails the build (avoids the goal-only false-green where 0 tests run but BUILD SUCCESS).
  • Redis pinned to redis:7-alpine.
  • test_speed_report.py reframed "Lift vs http4s" → "unit/pure vs integration" (all http4s now).
  • NEW run_tests_parallel.sh (+330) — local mirror of the sharded CI runner.

6. Test cleanup

Deleted migration scaffolding now dead: bridge-conversion suites (Http4sLiftBridge*, Http4sRequest/ResponseConversion*, Http4sCallContextBuilderTest); the cross-cutting property suites Http4sNativeRoutingPropertyTest (−2255) (its header / error-format / dispatch properties are guaranteed by the shared middleware and already verified by Http4s700RoutesTest) and AuthenticationPropertyTest (the lockout / login-attempt / provider logic is covered concretely by AuthenticationRefactorTest); v5 scaffolding (V500ContractParityTest, SystemViewsTests — its getSystemViewsIds 401/403/200 scenarios ported into Http4s500SystemViewsTest, CardTest, Http4s500RoutesTest). Added RetiredApiStandardsTest (guards retired endpoints stay retired), Http4sOpenIdConnectRoutesTest (5 scenarios) and Http4sOpenIdConnectSuccessTest.

7. Docs

LIFT_HTTP4S_MIGRATION.md, CLAUDE.md, FAQ.md, README.md updated to reflect the completed migration; todo_remove_duplicate_v7.0.0_endpoints.md deleted.

Reviewer notes

  • Confirm no /obp/* path Lift used to serve now silently 404s at notFoundCatchAll (the bridge previously caught these).
  • OIDC IO.blocking is a known limitation; exceptions surface as JSON, not a hung thread.
  • getCurrentUser → Empty: nothing in the runtime should rely on a Lift-session current user (portal is gone).
  • Verify shutdown-hook ordering vs the old LiftRules.unloadHooks (DB/connection cleanup).

Verification

  • Local (./run_tests_parallel.sh, default 4 shards): all shards green, ~5 min.
  • CI (8-shard workflow): all 8 shards green; 2,805 tests (surefire-aggregated across shards), every shard non-zero.
  • Lift-removal assertion: grep -rn "net.liftweb.http" obp-api/src/main/scala0 matches.

hongwei1 added 30 commits May 29, 2026 08:07
…overy off Lift

A1: add Http4sResourceDocAggregation (Lift-free cumulative per-version dedup);
    repoint ResourceDocsAPIMethods doc source from OBPAPINxx to Http4sNxx;
    force-init Implementations7_0_0 in Http4s700.allResourceDocs to fix
    resource-docs aggregation (v7 now returns full aggregated catalog, 830 docs).
A2: drop `extends LiftRules.DispatchPF` from ScannedApis (discovery via ClassScanUtils).
A3: remove the 12+1 LiftRules.statelessDispatch.append calls in enableVersionIfAllowed.

Full suite green: 2998 tests, 0 failures.
…SON catch-all

B1: remove Http4sLiftWebBridge.routes from Http4sApp; replace with notFoundCatchAll
    that returns {"code":404,"message":"..."} matching OBP error format.
B2: extract ensureStandardHeaders into Http4sStandardHeaders; add handleErrorWith
    500 handler in httpApp; remove bridge traffic audit route.
B3: delete Http4sLiftWebBridge.scala and the 5 bridge-specific test files
    (Http4sLiftBridgeTrafficTest, Http4sResponseConversionTest,
    Http4sCallContextBuilderTest, Http4sResponseConversionPropertyTest,
    Http4sRequestConversionPropertyTest).
    Remove wrappedRoutesV500ServicesWithBridge from Http4s500 (used only by
    the @ignore V500ContractParityTest, updated to use WithJsonNotFound variant).

Full suite green: 2902 tests, 0 failures.
Remove the Lift-typed partialFunction: OBPEndpoint first parameter from
ResourceDoc across all 48 construction sites (45 main + 3 test files).
All http4s call sites already passed null; dynamic-endpoint files passed
dynamicEndpointStub which is now obsolete for this purpose.

- ResourceDoc: remove partialFunction field; connectorMethods init → Nil
- getAllowedEndpoints / getAllowedResourceDocs: deleted (no live callers)
- getApiLinkTemplates: short-circuit to Nil (callers all commented out)
- ResourceDocsAPIMethods case _: drop partialFunction.getClass filter
- OBPRestHelper.registerRoutes: gut body to no-op
- versionRoutesClasses local val: removed (unused after case _ change)
Remove dead Lift Web configuration that no longer has any effect since
all request dispatch is now handled by native http4s:

- LiftRules.addToPackages("code")
- LiftRules.liftRequest.append (H2 console gate, dev/test only)
- LiftRules.early.append (UTF-8 charset)
- LiftRules.explicitlyParsedSuffixes
- LiftRules.localeCalculator (S.findCookie/ObpS.param locale logic)
- LiftRules.supplementalHeaders (X-Frame-Options, already in Http4sStandardHeaders)
- S.addAround(DB.buildLoanWrapper) (no Lift requests to wrap)
- UsernameLockedChecker object + LiftSession.on{BeginServicing,SessionActivate,SessionPassivate}
  (lockout is checked per-request in the http4s auth path)
- LiftRules.sessionInactivityTimeout
- Unused imports: LiftRules.DispatchPF, ObpS, SILENCE_IS_GOLDEN
Remove `routes: List[OBPEndpoint]` from VersionedOBPApis and ScannedApis
traits, along with all override declarations and registerRoutes(routes,...)
call sites across 27 aggregator files. The versionRoutes dispatch block in
ResourceDocsAPIMethods (all branches returned Nil) is deleted. Dynamic
endpoint/entity aggregators no longer import stale routes symbols from
OBPAPI5_0_0.
…m APIMethods traits

Delete aliveCheck.scala (replaced by AliveCheckRoutes) and AccountsAPI.scala
(Boot dispatch commented out, entire body already commented). Remove extends
RestHelper from DirectLogin (dlServe block deleted) and all APIMethods*
companion objects + traits (self: RestHelper => self-type removed). Drop
stale OBPEndpoint imports from aggregator files that no longer carry routes.
…ispatch

- OBPRestHelper no longer extends RestHelper; removed apiPrefix,
  failIfBadJSON, failIfBadAuthorizationHeader, oauthServe,
  buildOAuthHandler, override serve, getEndpoints, RichStringList
- registerRoutes simplified to a no-op stub (routes = Nil since C3c)
- Boot: ConnectorEndpoints.registerConnectorEndpoints disabled — Lift
  dispatch path removed in Phase B, not yet migrated to http4s
- ConnectorEndpoints: drop oauthServe import; make registerConnectorEndpoints
  a no-op matching Boot's disabled state
- OBPAPIDynamicEndpoint / OBPAPIDynamicEntity: remove stale
  apiPrefix/registerRoutes imports
- Delete `type OBPEndpoint` from APIUtil; replace with inline
  `PartialFunction[Req, CallContext => Box[JsonResponse]]` in the one
  remaining live site (ConnectorEndpoints).
- Remove `dynamicEndpointStub` val (only set on commented-out dynamic
  endpoint ResourceDocs; the http4s path never reads it).
- Delete `wrappedWithAuthCheck` method (~230 lines) — Lift-only auth
  wrapper superseded by ResourceDocMiddleware.
- Retype `ApiRelation`, `InternalApiLink`, `CallerContext` case classes:
  remove the `OBPEndpoint` fields (constructors are all commented-out at
  call sites); keep `rel`/`requestUrl`/`caller: String` so the types and
  their ArrayBuffer holders still compile.
- Delete `callEndpoint` helper (~45 lines) — Lift S.request / LAFuture
  plumbing with no http4s equivalent and no live callers.
- Purge dead `/* DISABLED */` block + `filterDynamicObjects` helper from
  APIMethodsDynamicEntity; drop the now-unused
  `import net.liftweb.http.{JsonResponse, Req}`.
- Remove stale `import code.api.util.APIUtil.OBPEndpoint` from 12 version
  aggregator files (OBPAPI1_3_0 … OBPAPI6_0_0, BG v1.3, BG v1.3 Alias).

Compilation clean after all changes.
…Methods300, DynamicEndpointHelper

- AccountsHelper: remove dead getFilteredCoreAccounts/filterWithAccountType
  (only called from commented-out Lift code); drop net.liftweb.http.Req import
- CustomAPIMethods300: remove extends RestHelper self-type;
  drop net.liftweb.http.rest.RestHelper import
- DynamicEndpointHelper: replace extends RestHelper with implicit val formats;
  drop net.liftweb.http.rest.RestHelper import
…h own type

Define APIUtil.HTTPParam (same shape as Lift's: name: String, values: List[String])
with a String-valued apply convenience constructor. Remove the lift-webkit
HTTPParam import from 24 files; also drop dead S.request.headers fallback in
getUserAndSessionContextFuture (Lift bridge removed in Phase B, always empty).
…) overload and S import

The second overload was only reachable from the Lift dispatch path (removed in phase B).
Cleans S, HTTPCookie, DirectLogin, Consumer, ResourceDoc, ObpS, and related dead imports.
…m I18NUtil

Cookie-based locale selection was dead code in the http4s path (no Lift session).
Simplified currentLocale() to only honour the PARAM_LOCALE query param, falling
back to the configured default locale.
…am in CurrencyUtil and fx

LiftRules is a Lift-Web type; replaced with standard getClass.getResourceAsStream so
these files no longer depend on net.liftweb.http.
…ourceBundle in translatedErrorMessage

Removes LiftRules and TransientRequestMemoize from OBPRestHelper; translatedErrorMessage
now uses a plain Java ResourceBundle.getBundle("i18n.lift-core", locale) lookup, which
produces identical results without requiring a live Lift request scope.
…kie from search.scala

searchProxy now returns JValue directly; searchProxyV300/StatsV300 return Box[JValue];
ESJsonResponse is a plain case class. Http4s200 updated to use the simplified API.
… ObpS to http4s stub

OAuth.scala: dead unused import removed.
Helper.scala: CGLIB proxy of Lift's S replaced with a plain Scala object whose methods
return Empty/default, matching the actual runtime behaviour on the http4s path where
the Lift S scope is never initialised.
Deletes connectorEndpoints lazy val, JsonAny object, and extends RestHelper —
all dead since Phase B removed the Lift bridge. Removes JsonResponse/Req/RestHelper
imports from net.liftweb.http.
… ResourceDocsAPIMethods

Replace LiftRules.loadResourceAsString with getClass.getResourceAsStream; remove
unused S, InMemoryResponse, PlainTextResponse imports.
…IUtil

- Remove bulk `net.liftweb.http._` import; keep narrow `JsonResponse` only
- Replace `LiftRules.getResource` with `getClass.getResourceAsStream`
- Replace `JsRaw("")` (4 sites) with `JNull`
- Replace `s.request` in GatewayLogin.validator call with `Empty` (S never
  initialised in http4s path)
- Delete dead Lift-dispatch helpers: futureToResponse, futureToBoxedResponse,
  getFilteredOrFullErrorMessage, scalaFutureToJsonResponse,
  scalaFutureToBoxedJsonResponse, getRequestBody(Box[Req])
- Delete RestContinuation.async-based internal async utilities
- Stub S-dependent helpers to http4s-safe defaults (getCorrelationId,
  getRemoteIpAddress, httpMethod, getUserAndSessionContextFuture etc.)
…ogin

- GatewayLogin: change validator/getAllParameters signature from Box[Req] to
  Box[String] (Authorization header value); remove import net.liftweb.http._
- GatewayLogin.getUser: replace S.request with Empty (S never initialised
  in http4s path)
- dauth: replace S.getRequestHeader with Empty; remove import net.liftweb.http._
- directlogin: replace S.request usages with safe defaults ("GET"/"POST");
  getAllParameters returns MissingDirectLoginHeader error (Lift bridge removed);
  remove import net.liftweb.http._
- APIUtil: update GatewayLogin.validator call to pass cc.authReqHeaderField
  (the actual Authorization header value from CallContext)
…esponse

Define a lightweight code.api.JsonResponse case class (body: JValue,
headers, cookies, code) to replace Lift's net.liftweb.http.JsonResponse
as the common error-carrier type.  The http4s middleware
(ErrorResponseConverter, JsonResponseExtractor) already read body+code
so the switch is transparent.

- OBPRestHelper: add JsonResponse + JsonResponse.apply(JValue,Int) definitions;
  remove import net.liftweb.http.JsonResponse
- APIUtil: import code.api.JsonResponse; replace .toJsCmd with compactRender;
  remove .asInstanceOf[JsonResponse] casts
- NewStyle: import code.api.JsonResponse
- Http4s500: replace .toResponse.data with compactRender(body)
- Boot: rename Lift's JsonResponse → LiftJsonResponse to resolve import
  ambiguity; update exceptionHandler + uriNotFound call sites
- AuthUser: rename Lift's JsonResponse → LiftJsonResponse to resolve import
  ambiguity (AuthUser itself does not use JsonResponse directly)
Replace net.liftweb.http.provider.HTTPParam with the Lift-free drop-in
replacement defined in APIUtil so test-compile passes after the
net.liftweb.http removal in main sources.
…m Boot.scala

Lift no longer handles any HTTP requests (bridge removed in Phase B), so
the exceptionHandler and uriNotFound prepend blocks are dead code. Also
remove the two private helpers (showExceptionAtJson, sendExceptionEmail)
that were only called from those blocks.

Simplify the import from 'net.liftweb.http.{JsonResponse => ..., _}'
to 'import net.liftweb.http.LiftRules' — only LiftRules.unloadHooks,
LiftRules.context, and LiftRules.getResource remain active.
…BPRestHelper

The implicit unwrapped Box[JsonResponse] into JsonResponse for the Lift
dispatch path. Since dispatch is fully replaced by http4s and no Lift
endpoint is served any more, this implicit is never triggered.

Removing it eliminates the last live usage of Box[Failure/Empty/ParamFailure]
inside OBPRestHelper and shrinks the class surface.
…n hooks

Replace LiftRules.unloadHooks.append with Runtime.getRuntime.addShutdownHook
so DB/Redis/gRPC cleanup fires on JVM termination regardless of whether
the Lift servlet lifecycle runs (it does not in the IOApp deployment).

Replace LiftRules.context.path (servlet context path) with '' since
http4s runs at root and the old servlet-context fragment was always
empty in OBP deployments.

Replace LiftRules.getResource('/') with getClass.getClassLoader.getResource
so the use_custom_webapp copy logic works without a servlet container.

Boot.scala no longer imports net.liftweb.http — only AuthUser.scala
(C5 scope) and commented-out legacy code retain that dependency.
S.request is always Empty in the http4s path (no Lift session), so the
authorization and directLogin variables derived from it were always Empty.
Replace with literal Empty to eliminate the S.request dependency in the
active code path — portal redirect methods (logout, signup) still use
S.request but are dead code in API-only mode.
….http imports

Remove the three net.liftweb.http imports (S.fmapFunc, wildcard http,
sitemap.Loc.*) from AuthUser.scala. All callers were dead portal code:

- Remove _toForm overrides in MyFirstName/MyLastName/userName/MyPasswordNew/email
  (used fmapFunc — now gone, enabling import removal)
- Remove S.notice/S.error side-effect calls from sendValidationEmail,
  validateUser, actionsAfterSignup (no-op in API-only http4s path)
- Remove S.? i18n lookups; replace with literal fallback strings
- Delete failedLoginRedirect SessionVar (no external callers since
  OAuthAuthorisation.scala setter was commented out)
- Delete userLoginFailed (no callers anywhere in main sources)
- Gut signup/loginMenuLocParams to empty/Nil stubs
- Simplify logout to logoutCurrentUser + net.liftweb.http.S.redirectTo
  (one fully-qualified call, no import needed; keeps Nothing return type
  required by ProtoUser trait contract)

Remaining net.liftweb.http reference in AuthUser is the single
net.liftweb.http.S.redirectTo in logout — unavoidable without replacing
MegaProtoUser/MetaMegaProtoUser (out of scope per C5 landmine note).
…ridge comments, rename test files

- OBPRestHelper: Delete the `registerRoutes` stub, which has no active callers.
- Http4sApp: Replace residual "Lift bridge" / "Lift fallback" phrasing within comments for `corsHandler`, `dynamicEndpoint`, `UK OB`, and `body-caching` with accurate descriptions
(referring to the http4s version-cascade bridge and `notFoundCatchAll`; the actual Lift bridge has been removed).
- Http4sServerIntegrationTest: Correct "Lift bridge" phrasing in scenario names to
"http4s version-cascade"; remove references to non-existent documentation files.
- Http4sLiftBridgePropertyTest → Http4sNativeRoutingPropertyTest:
Remove redundant scenarios 7.1/7.2/7.4, which completely overlap with Properties 6.6/9.6;
Replace all Lift bridge-related phrasing in the class name, Tags, and `feature`/`scenario` descriptions
with http4s native routing / version-cascade terminology; assertions remain unchanged.
- CLAUDE.md: Synchronize updates to two references pointing to old filenames/class names.
…ge removal

The Lift fallback bridge (Http4sLiftWebBridge) was removed, but CLAUDE.md still
described it as a live part of the routing chain in 6 places. Updated to reflect
the real Http4sApp.baseServices chain, which terminates in notFoundCatchAll
(JSON 404) with no Lift fallback:

- Request priority chain: replaced the obsolete short chain with the actual
  route order; stated explicitly there is no Lift fallback.
- system-views empty-segment gotcha: request falls through to notFoundCatchAll,
  not the Lift bridge.
- bridge-cascade hijack note: rewritten as past tense; the Lift dedup safety net
  is gone, un-migrated overrides now cascade to older http4s handlers or 404.
- API1_2_1Test perf note: now fully native http4s.
- per-version completeness table: all versions show 0 active Lift handlers;
  noted v3.1.0 getMessageDocsSwagger / getObpConnectorLoopback are http4s-served.
- v6.0.0 migration note: wired as v600Routes; dropped "ahead of the Lift bridge".

Documentation only; no source changes, no behavior change.
hongwei1 added 18 commits June 2, 2026 21:49
The After column still showed ResourceDoc(null, ..., http4sPartialFunction = Some(root)). The null first-arg (OBPEndpoint partialFunction) was removed in f1d3544; align with the current signature (first param implementedInApiVersion), matching the CLAUDE.md Rule 1 fix.
…suite)

ServerSetupWithTestData and ServerSetupWithTestDataAsync now skip creating the per-scenario 100 transactions + 200 transaction-requests unless the suite actually reads them (suitesNeedingTransactionData whitelist, matched by simple class name). The ~235 suites that never touch this data save ~300 DB writes per scenario. Full suite ALL SHARDS PASSED (5m39s vs 7m16s baseline) with per-test time down across the board. The only surefire flakies are pre-existing Redis-contention rate-limit/cache tests (4 shards share one Redis DB), unrelated to this change.
… flakiness

Parallel test shards shared one Redis DB, so rate-limit counters and cache keys collided across shards and each scenario's FLUSHDB wiped other shards' state - causing intermittent '429 did not equal 200' and cache-test failures. Each shard now sets a distinct api_instance_id (OBP_API_INSTANCE_ID=shard_N), which flows through Constant.getGlobalCacheNamespacePrefix into every Redis key, isolating shards on the shared Redis. wipeTestData replaces FLUSHDB with Redis.deleteKeysByPattern(namespace + '*') so a shard only clears its own keys. (The earlier JedisPool DB-index approach was reverted: its 6-arg overload changed connection behaviour and broke ~20 suites; this key-namespace approach does not touch JedisPool.) Full suite ALL SHARDS PASSED (5m4s), ZERO 429s, all 4 Redis-contention suites green - down from 5 flaky to 1 unrelated pre-existing CounterpartyTest 'head of empty list'.
…nistic failure)

CounterpartyTest reads otherAccountsJson.other_accounts.head (CounterpartyTest.scala:38); the other-accounts/counterparties are derived from beforeEach-created transactions' metadata. It was missing from suitesNeedingTransactionData, so after the on-demand-data change it received no transactions, other_accounts was empty, and .head threw 'head of empty list' deterministically — masked as flaky because scalatest did not halt the build. Adding it to the whitelist restores its transaction data. Full suite now truly green: ZERO surefire failures across all 4 shards (5m30s).
The Lift -> http4s migration is complete: every API version (v1.2.1 through
v7.0.0) is served by http4s, so the "http4s vs Lift" framing of the per-test
speed report is misleading -- there is no Lift HTTP code left. The "Lift vN"
labels described which API version a suite tests, not which framework runs it.

Rework test_speed_report.py to report two orthogonal, accurate views:
  1. By execution model -- unit/pure (no server) vs integration (embedded
     server). Integration is detected by scanning obp-api/src/test/scala for
     test classes that extend ServerSetup, plus an explicit list of
     self-starting http4s server suites. Degrades gracefully (counts suites as
     integration) when sources are absent. This is the real migration KPI:
     logic that used to need a running server is now pure unit-tested.
  2. By API version -- API v1..v7, all served by http4s. Drops the "Lift vN"
     labels.

Rename the report job step in both workflows accordingly. No test code is
changed; this only affects how the CI report classifies and labels suites.
The Lift -> http4s HTTP-layer migration is complete, so the two large
migration-era property suites that verified "http4s behaves like Lift" are now
redundant with regular coverage:

- Http4sNativeRoutingPropertyTest (2255 lines): cross-cutting routing-layer
  properties (header injection, error JSON format, dispatch, cross-version
  exception format) are enforced by single shared middleware
  (Http4sStandardHeaders, ErrorResponseConverter) and already verified by
  Http4s700RoutesTest's "response headers" feature + Http4sServerIntegrationTest;
  its auth scenarios duplicate DirectLoginTest / gateWayloginTest.
- AuthenticationPropertyTest (1682 lines): the lockout / login-attempt /
  provider-routing logic is covered concretely by AuthenticationRefactorTest
  (21 scenarios) plus DirectLoginTest / LockUserTest /
  VerifyExternalUserCredentialsTest.

Http4sServerIntegrationTest is KEPT as the genuine end-to-end smoke layer.

Also consolidate the duplicated v5.0.0 SystemViews suites: port the
getSystemViewsIds 401/403/200 scenarios into the http4s-native
Http4s500SystemViewsTest and remove the Lift-era SystemViewsTests (both
exercised the same, now-http4s, handlers). Clean a stale suite reference in
test_speed_report.py.

Verified: full obp-api test-compile passes (no breakage from deletions) and
Http4s500SystemViewsTest is green (16/16, including the 3 ported scenarios).
Follow-up cleanup after the migration-era property-suite removal:

- Http4s700RoutesTest: remove the empty feature("Http4s700 routing priority")
  block (dead scaffolding -- a comment but no scenarios).
- V7ResourceDocsAggregationTest: the aggregation bug these scenarios were
  written against is fixed and they now pass, so rename the misleading
  "(EXPECTED TO FAIL)" / "(MAY FAIL)" scenario titles and reword the
  "MUST FAIL on unfixed code" scaladoc/info lines to describe them as the
  regression guards they now are. Assertions unchanged.

No behavior change; obp-api test-compile passes.
Optimize build_container.yml (push) and build_pull_request.yml (PR) to cut
wall-clock from ~21min toward ~10min:

- compile: add actions/cache for target/classes+test-classes+scala-2.12 (Zinc
  incremental analysis) and drop `clean`, so unchanged sources are not
  recompiled (full ~238s -> incremental ~80-120s on small diffs). Unique cache
  key per commit with a zinc- restore-keys prefix fallback to the latest cache.
- test: split the 4-shard matrix into 8 rebalanced shards (v4_0_0 isolated as
  the bottleneck package; catch-all moved to shard 8) to lower the slowest shard
  from ~562s toward ~290s.
- test: run 'mvn scalatest:test -pl obp-api -DfailIfNoTests=false' (goal-only)
  instead of 'mvn test', bypassing the compile/test-compile lifecycle phases
  (classes already supplied via the compiled-output artifact), matching
  run_tests_parallel.sh.
- build_pull_request.yml: remove the 'if: github.repository == OpenBankProject'
  guard on the compile job so PR builds run on this fork instead of skipping the
  whole pipeline.
The HTTP-layer Lift -> http4s migration left 95 net.liftweb.http references in
obp-api/src; this removes all of them, reaching ZERO.

- AuthUser.scala: delete the dead `override def logout` (the last active
  net.liftweb.http call, S.redirectTo). It had zero callers -- Lift SiteMap /
  Menu / login UI is disabled (API-only mode) so Lift never invoked it; http4s
  logout uses the inherited AuthUser.logoutPath via GET /users/current/logout-link,
  which is unaffected. Dropping the override lets the inherited (unreachable)
  MegaProtoUser.logout apply.
- 91 commented-out `//import net.liftweb.http...` lines across 80 files: dead
  import comments in the top-of-file import region, removed via an anchored
  line-precise pass that cannot touch any ResourceDoc/endpoint comment block.
- 3 doc-strings (OBPRestHelper, DynamicCompileEndpoint, APIUtil): reworded to
  drop the net.liftweb.http reference while keeping the description.

Verified: grep -rn "net.liftweb.http" obp-api/src -> 0; obp-api test-compile
BUILD SUCCESS; ResourceDoc parity audit byte-identical before/after;
DirectLoginTest + AuthenticationRefactorTest green.
The previous commit switched test shards to 'mvn scalatest:test' (goal-only),
which skips the process-resources phase. The test.default.props generated by the
Setup props step is therefore never copied into target/classes (the classpath),
so ScalaTest's Props lookup fails, Constant init throws, suite discovery aborts:
0 tests run but the build reports SUCCESS (false green; shard Run-tests step was
15s vs the expected ~258s).

Revert to mvn test: its process-resources phase places the props on the
classpath, while the touch trick + cached target/ keep Zinc from recompiling so
lifecycle overhead stays minimal. The 8-shard split and Zinc compile cache (the
real speedups) are retained.
…eanup

The dead AuthUser.logout override (the last vestigial Lift HTTP call) and the
91 commented-out import lines were removed, so OBP .scala source now has zero
Lift HTTP references. Update the milestone note (drop the stale "one vestigial
S.redirectTo survives / cleanup candidate" wording) and correct the
remaining-Lift line to list the deliberately-kept non-web libraries
(lift-mapper / lift-json / lift-common / lift-util) instead of only lift-mapper.
…eep props)

mvn test spends ~208s/shard on the multi-module compile/testCompile lifecycle
(Zinc full scan even when nothing recompiles + the obp-commons reactor). Switch to
'mvn process-resources scalatest:test -pl obp-commons,obp-api': process-resources
still copies the dynamically generated test.default.props onto the classpath
(fixing the false green that plain goal-only scalatest:test caused), while the
compile/testCompile phases are skipped (classes come from the compiled-output
artifact + touch trick).

Incremental-compile (no-clean) cache verified safe: a deletion experiment showed
scala-maven-plugin 4.8.1 removes orphan .class files on incremental rebuild (no
residual, no jar leakage), so the Zinc cache is retained.

Expected: slowest shard ~450s -> ~250s, wall-clock ~14min -> ~10min.
Experiment only (build_container.yml). Splits the 8-shard matrix into 12 to
measure whether more shards reduce wall-clock. Hypothesis: NO, because the floor
is compile (~350s, serial) + the two unsplittable big packages — code.api.v4_0_0
(~254s) and code.api.v1_2_1 (single API1_2_1Test suite, ~242s). v1_2_1 and v4 each
get their own shard here; if wall-clock stays ~10min this proves shard count is no
longer the bottleneck. Will revert to 8 if not faster (more shards also = more
redis docker-pull flakiness).
…ache experiments)

Both experiments measured on real CI runs and rejected:
- 12 shards (run 26859975836): wall-clock 630s vs 8-shard 622s -- NOT faster, even
  slightly slower. v4_0_0 (~254s) and v1_2_1 (single API1_2_1Test suite, ~281s) are
  unsplittable by package prefix, so each just becomes its own shard's floor; more
  shards only add redis docker-pull flakiness.
- Zinc incremental cache (no-clean): a zero-source-change rebuild was still 333s vs
  a full 350s -- only ~17s saved, because compile is dominated by Maven startup,
  dependency resolution, packaging the ~297MB fat jar, and install, not Scala
  compilation. Not worth the stale-risk class of no-clean builds.

Final config: 8 shards + goal-only (process-resources scalatest:test, ~208s/shard
saved) + mvn clean install. ~10min wall-clock, the floor set by compile (~350s
serial) + the largest unsplittable test shard.
…light shards

The 10x stability run showed shard2 (v1_2_1+berlin+management+metrics) was the
slowest at 314s while shards 7/8 sat at 87s/103s. v1_2_1 (single API1_2_1Test
suite) alone is ~281s; the extra packages pushed it to 314s and set the critical
path. Isolate v1_2_1 on its own shard; move berlin to shard 7 and management/metrics
to shard 8 (both well under 281s). Expected slowest test shard 314s -> ~281s,
wall-clock ~676s -> ~640s. Same total test count, no package dropped.
The 10x stability run showed code is fully stable (10/10 with 0 test failures) but
1/10 runs had a shard fail at Initialize containers -- the redis service image pull
from docker.io timed out. Pin to redis:7-alpine (~30MB vs latest ~140MB): smaller
image = shorter pull = smaller timeout window, plus a fixed version for
reproducibility. OBP uses only basic redis commands (rate-limit counters, cache),
fully supported by alpine.
@hongwei1 hongwei1 reopened this Jun 3, 2026
@hongwei1 hongwei1 changed the title Refactor/remove liftweb code refactor: remove Lift Web framework — full http4s runtime + OIDC migration Jun 3, 2026
hongwei1 and others added 8 commits June 3, 2026 10:43
…ter)

compile was 315s, ~100s of which is maven-shade packaging the 297MB executable fat
jar -- which the test shards do not use (they only need target/classes). Make shade
skippable (root pom <shade.skip> property, default false so local/release builds are
unchanged; obp-api shade <skip>${shade.skip}</skip>), then:
- compile job: add -Dshade.skip=true -> only compiles classes (~315->~215s)
- new parallel jar job (needs compile, runs alongside test shards): downloads the
  compiled classes and runs shade to build the fat jar -> ${sha} artifact, off the
  test critical path
- docker: needs [test, jar], pulls the fat jar from the jar job
- build_pull_request.yml: compile -Dshade.skip=true, no jar job (PR only runs tests)

Expected: critical path 607 -> ~500s (compile 215 + slowest test shard 275 + report).
…cess-path test

The Lift->http4s teardown removed S.addAround(DB.buildLoanWrapper), which gave each
request a single DB transaction. The OIDC callback runs synchronous Lift Mapper writes
inside IO.blocking and does not pass through ResourceDocMiddleware's
withBusinessDBTransaction (the request-scoped proxy/TTL is null on the blocking thread),
so its six provisioning writes (resource user, auth user, entitlements, consumer, OIDC
token, DirectLogin token) each auto-committed independently.

M1: wrap the provisioning block in DB.use(DefaultConnectionIdentifier) so all writes
share one connection and a thrown DB error rolls the whole set back -- faithfully
restoring the removed buildLoanWrapper semantics (it likewise committed on
logical-failure returns and only rolled back on a thrown exception). Token exchange and
JWKS validation stay outside the transaction so no connection is held during remote HTTP.

M2: add Http4sOpenIdConnectSuccessTest -- a self-contained success-path test that stands
up a local stub provider (token + JWKS endpoints), signs an RS256 id_token with Nimbus,
drives the callback in-process, and asserts 200 {token} plus that the resource user was
provisioned. This also exercises the M1 transaction wrapping.

Also fix a stale comment that referenced the removed Lift bridge.
…d connector-export endpoint

NEW-1 (test coverage hole): the 8-shard runner matches suites by FQN prefix
(-DwildcardSuites). The catch-all marks the parent package code.api as "covered" the
moment any child (code.api.v4_0_0, ...) is assigned, so it never appends bare code.api;
test classes sitting DIRECTLY in package code.api therefore run only if a shard filter
prefixes them explicitly. AliveCheckRoutesTest, Http4sOpenIdConnectRoutesTest and
Http4sOpenIdConnectSuccessTest matched no filter and ran in NO shard -- the OIDC tests
this branch added (incl. the success-path test) were never executed by CI. Add
code.api.AliveCheckRoutesTest and code.api.Http4sOpenIdConnect to shard 8's test_filter in
both build_pull_request.yml and build_container.yml. Verified locally that the prefix
discovers and runs all 6 OIDC scenarios.

NEW-2 (known gap): record the prop-gated connector.name.export.as.endpoints
(/connector/{methodName}) Lift endpoint -- removed with the Lift teardown, not ported to
http4s, no replacement -- as a known gap in LIFT_HTTP4S_MIGRATION.md so it isn't silently
lost.
Boot registered two separate Runtime.getRuntime.addShutdownHook threads -- DB/Redis
close (early in boot) and gRPC server.stop() (in the grpc.server.enabled block). The
JVM runs all shutdown hooks CONCURRENTLY with no ordering guarantee, so with gRPC
enabled the two could race: a still-draining gRPC in-flight request touching the DB
while the connection pool is being closed.

Combine into a single ordered hook: stop gRPC first (drains in-flight), then close DB,
then Redis -- each step guarded so one failure doesn't skip the rest. The gRPC server
is now held in an Option (grpcServerOpt); when disabled the hook still closes DB/Redis.

object ToSchemify (which hosts the gRPC start block) now extends MdcLoggable so the
hook can log step failures.

Only manifested with grpc.server.enabled=true (default false).
…ion-state caveat

Security: exchangeAuthorizationCodeForTokens logged client_secret (inside `data`),
the raw token-endpoint response, and id/access/refresh token values at debug level.
Scrubbed all four sites to log only non-sensitive metadata (endpoint URL, success
flag, tokenType/expiresIn/scope).

Doc: portal-login removal left sessionState="" while openid_connect.check_session_state
defaults to true (fail-closed), so a real callback state is rejected with 401 unless
deployments set it false. This fail-closed default is intentional (locked by
Http4sOpenIdConnectRoutesTest), so it is kept as-is; added a CONFIG CAVEAT comment in
code + sample.props so operators know to set false.

Verified: Http4sOpenIdConnectRoutesTest + Http4sOpenIdConnectSuccessTest +
DirectLoginTest + gateWayloginTest green (22/22).
The 8-shard layout puts the catch-all on shard 8, but the log line still
hardcoded "Catch-all extras added to shard 4" (leftover from the old 4-shard
layout), so the run log misreported which shard absorbed the unassigned
packages (code.external, code.setup, com.openbankproject.commons.util). Use
${{ matrix.shard }} so it prints the actual shard. Log text only — no behavior
change; EXTRAS was already appended to the correct catch-all shard.
…s_parallel.sh

gtimeout only exists on macOS (Homebrew coreutils); on Linux the binary
is timeout, so every shard failed immediately with 'gtimeout: command
not found' (rc 127) before mvn ran. Detect whichever binary is present.
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud Bot commented Jun 4, 2026

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@simonredfern simonredfern merged commit c3fb4dd into OpenBankProject:develop Jun 4, 2026
10 of 11 checks passed
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.

3 participants