Skip to content

[Feature] Deduplicate HTTP servlet stacks with cursor filters and a declarative endpoint registry #6922

Description

@SeriousCoding789

Summary

java-tron currently maintains the same HTTP API surface four times: the default FullNode service, the Solidity service, the PBFT service and the standalone SolidityNode service. The Solidity and PBFT stacks consist of ~96 wrapper servlets whose only job is switching the per-thread read cursor, and every service keeps its own hand-written path-to-servlet registration list.

This proposal removes the duplication in two steps — both are part of this proposal; the ordering only serves a safe rollout:

  • Step 1 — cursor filters: replace all cursor-switching wrapper servlets with one servlet Filter per cursor service, so all services share the single base servlet implementations (removes ~98 classes, ~2,900 lines, zero behavior change).
  • Step 2 — endpoint registry: replace the four hand-written registration lists with an @HttpApi annotation on the servlet that serves each endpoint (path suffix → access nature → exposed surfaces), from which a read-only registry is derived at class-load and every service builds its mappings; enforce at startup that non-read endpoints exist only on the FullNode surface.

Problem

Motivation

Adding or changing one HTTP endpoint today requires synchronized edits in up to 4 wrapper classes and 4 registration lists. Missing one spot silently produces feature drift: the same endpoint behaves differently (or is missing) depending on which service serves it.

Current State

  • interfaceOnSolidity.http contains 49 wrapper servlets and interfaceOnPBFT.http contains 47, each of the form:
public class GetAccountOnSolidityServlet extends GetAccountServlet {
  @Autowired private WalletOnSolidity walletOnSolidity;
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
    walletOnSolidity.futureGet(() -> super.doGet(req, resp));
  }
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
    walletOnSolidity.futureGet(() -> super.doPost(req, resp));
  }
}
  • WalletOnSolidity / WalletOnPBFT extend WalletOnCursor; futureGet is a same-thread bracket:
dbManager.setCursor(cursor);   // SOLIDITY or PBFT (PBFT offset computed per call)
try { run(); } finally { dbManager.resetCursor(); }
  • The underlying read cursor (Chainbase.cursor) is a ThreadLocal: it only selects the snapshot the current thread starts reading from, and is invisible to every other thread. Block processing, sync and broadcast run on dedicated executors that never touch the cursor, and each Jetty port owns a separate thread pool.
  • The standalone SolidityNode's SolidityNodeHttpApiService is a wholesale copy of HttpApiOnSolidityService — the same wrapper servlets and registration, just with a different cursor wrapper. The PBFT service (HttpApiOnPBFTService) checks out the same way: verified endpoint by endpoint, it serves the same read-only interface as those two (bar the drift noted below), just with the PBFT cursor wrapper.
  • Registration lists are hand-written per service: 123 mappings on the default FullNode service, 45 on the Solidity service, 47 on the PBFT service, 45 on the standalone service — 128 unique endpoints declared 260 times.

Limitations or Risks

  • Maintenance burden: ~98 wrapper/forked classes are pure boilerplate that must track every base servlet change.

  • Feature drift is real, not hypothetical: the two SolidityNode forks missed later improvements of the base servlets — standard JSON error responses and visible=true log address conversion — so the same endpoint already answers differently across deployments:

    // fork (GetTransactionInfoByIdSolidityServlet): logs stay hex even when visible=true,
    // and errors are written as raw text
    response.getWriter().println(JsonFormat.printToString(transInfo, visible));
    ...
    } catch (Exception e) {
      response.getWriter().println(e.getMessage());
    }
    
    // base (GetTransactionInfoByIdServlet): converts log addresses for visible=true,
    // and errors go through the standard JSON error body
    transactionInfo = transactionInfo.toBuilder().clearLog()
        .addAllLog(Util.convertLogAddressToTronAddress(transactionInfo)).build();
    ...
    } catch (Exception e) {
      Util.processError(e, response);
    }

    The endpoint sets of the Solidity and PBFT services also differ in ways documented nowhere: 2 read-only endpoints — getpaginatednowwitnesslist and gettransactioninfobyblocknum — exist only on the Solidity service, and 5 dormant shielded-TRX endpoints existed only on the PBFT service:

    Path (on the PBFT service only) Servlet
    /walletpbft/getmerkletreevoucherinfo GetMerkleTreeVoucherInfoServlet
    /walletpbft/scanandmarknotebyivk ScanAndMarkNoteByIvkServlet
    /walletpbft/scannotebyivk ScanNoteByIvkServlet
    /walletpbft/scannotebyovk ScanNoteByOvkServlet
    /walletpbft/isspend IsSpendServlet

    These trace to a 2020 cleanup that commented the shielded-TRX endpoints out on the other services but missed the PBFT service created five weeks earlier, and the leftover went unnoticed for six years because the PBFT service is disabled by default. Building the registry surfaced this immediately; the oversight is confirmed and fixed as part of this work.

  • Convention-only safety: nothing enforces that cursor services expose read-only endpoints. A registration mistake exposing a write endpoint on the Solidity or PBFT service would let a cursor-switched thread enter the write path (Chainbase.put and SnapshotManager.advance resolve through the cursor-aware head()).

Proposed Solution

Proposed Design

Step 1 — cursor filters (equivalent refactor).

One abstract filter reproduces the futureGet bracket at the transport entry; two @Component subclasses parameterize the cursor:

public abstract class WalletCursorFilter implements Filter {
  public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
      throws IOException, ServletException {
    try {
      dbManager.setCursor(cursor);   // Solidy/PBFT offset computed inside Manager#setCursor per request
      chain.doFilter(req, resp);
    } finally {
      dbManager.resetCursor();       // always back to HEAD: no residue on pooled jetty threads
    }
  }
}

The filter is appended (after the existing liteFnQueryHttpFilter / httpApiAccessFilter) at /* of the Solidity and PBFT contexts, so every request on a cursor service — including the /wallet/getnodeinfo alias on the Solidity service — reads the corresponding state view. The services then register the base servlet beans on unchanged path strings, and all wrapper servlets are deleted. Equivalence holds because the bracket is same-thread and always resets, exactly like futureGet; the standalone SolidityNode service likewise drops its 2 forks in favor of the base servlets.

One deliberate alignment is worth calling out: the triggerconstantcontract / estimateenergy wrappers wrap super.doPost in a try/catch (IOException) that logs and swallows the exception — not by design, but because futureGet(Runnable) takes a lambda that cannot throw checked exceptions. With the filter there is no lambda, so this workaround disappears: on the Solidity / PBFT services an IOException from response writing now propagates to jetty exactly as it already does on the default FullNode service. The only affected scenario is a client that dropped the connection mid-response; the three services become uniform instead of subtly different.

The read-only invariant. A thread whose cursor is set to SOLIDITY / PBFT must never enter a write path. The cursor does not only redirect reads: Chainbase.put()/delete() and SnapshotManager.advance()/retreat() all resolve their target snapshot through the cursor-aware head(), so a cursor-switched thread executing a write (e.g. a broadcast calling buildSession()) would write into the solidified view or advance the shared snapshot chain from the wrong position — corrupting state that every thread sees. This is unchanged from the existing wrapper mechanism; the filter neither widens nor narrows it. Today the invariant holds because both cursor services expose only read-only endpoints (audited endpoint by endpoint: all handlers are pure queries, and the two constant-call endpoints buffer VM writes in an in-memory Repository that is discarded without commit) — but it holds by convention only. The standalone SolidityNode surface carries the same read-only requirement for a different reason: that process only syncs solidified blocks from a FullNode and has no path to propagate a transaction into the network, so a write endpoint there could only strand transactions (today all of its 45 endpoints are read-only as well). The general rule — any non-read endpoint may exist only on the FullNode surface — is exactly what Step 2 turns into a boot-time check.

Step 2 — endpoint registry derived from annotations (single source of truth).

Each endpoint is declared once, on the servlet that serves it:

@Component
@HttpApi(value = "getaccount", access = Access.READ,
    surfaces = {Surface.FULL, Surface.SOLIDITY, Surface.PBFT, Surface.SOLIDITY_NODE})
public class GetAccountServlet extends RateLimiterServlet { ... }

@Component
@HttpApi(value = "broadcasttransaction", access = Access.WRITE, surfaces = Surface.FULL)
public class BroadcastServlet extends RateLimiterServlet { ... }   // write endpoints stay FULL-only

@Component
@HttpApiExcluded("sapling shielded note-scan API, disabled on every surface")
public class ScanNoteByIvkServlet extends RateLimiterServlet { ... }   // opting out is explicit

HttpApiRegistry derives the whole table at class-load by scanning org.tron.core.services.http.servlets for HttpServlet subclasses and reading their declared annotations. No hand-maintained list repeats the metadata: the declaration sits on the implementation, so an endpoint cannot drift between what it does and where it is registered. Each service's addServlet collapses into one loop over HttpApiRegistry.forSurface(...), resolving servlet beans from the application context (which also removes the ~40 injected servlet fields per service).

Two properties of the annotation carry the guarantee, and both are asserted at startup rather than left to review:

  • @HttpApi is not @Inherited, and is read only through Class#getDeclaredAnnotation. Servlets here have historically been subclassed to vary behaviour — precisely what Step 1 deletes. If exposure were inheritable, or were looked up with a superclass-walking helper such as Spring's AnnotatedElementUtils#findMergedAnnotation, a future subclass would silently inherit its parent's suffix, access and surfaces, and a WRITE endpoint could reach a cursor surface without anyone declaring it.
  • Every concrete servlet in the package carries exactly one of @HttpApi or @HttpApiExcluded. Without that rule a servlet could be added and simply never registered — the silent-omission failure this refactor exists to remove, merely moved from the registration lists into the annotations. Opting out therefore has to be written down, with a reason, on the class itself.

The table is built and validated the first time the registry is touched — while a service mounts its servlets, before jetty binds any port, and across all four surfaces whether or not the node enables them. Any violation aborts the boot with TronError(API_SERVER_INIT) rather than dropping an endpoint quietly:

  • a non-READ endpoint declared on a surface other than FULL (covering both cursor services and the solidified-only standalone SolidityNode);
  • a duplicate (surface, suffix) pair;
  • a suffix that is not a single path token — the suffix is concatenated into a jetty path spec, so * would mount the servlet as a prefix wildcard swallowing every sibling endpoint under the same prefix;
  • an endpoint declared on a class that can never be mounted (abstract, or nested inside another class);
  • a servlet that is not a Spring @Component bean.

Rollout is one service per commit — PBFT (most regular) → Solidity (handles the dual-mount alias) → standalone SolidityNode → FullNode (largest, carries all BUILD/WRITE rows) — each verified by a 1:1 endpoint diff against the previous hand-written list. The parity tests are then derived from the registry as well: each service's mounted path set must equal the registry's set for that surface, so an endpoint can be neither declared-but-unmounted nor mounted-but-undeclared.

Current shape: 120 servlets declare @HttpApi and 10 declare @HttpApiExcluded; the derived surfaces are FULL 120, SOLIDITY 44, PBFT 44, SOLIDITY_NODE 44.

The same declaration also lets an accidental gap in a read surface be closed. The two endpoints found only on the Solidity service — getpaginatednowwitnesslist and gettransactioninfobyblocknum — are read-only and not surface-specific: each answers a general query against whatever snapshot the current cursor selects, with no behavior tied to a particular service. Read-only means they are safe to serve from a cursor service (no write-path risk); being general-purpose rather than dedicated interfaces means there is no functional reason to scope them to Solidity alone. They are therefore declared on the PBFT surface as well — not on a blanket rule that every read must appear on every surface (a read may legitimately be surface-specific), but because these two have no reason to be withheld from PBFT. This also keeps the two transports in step: the follow-up gRPC merge (see References) serves RpcApiServiceOnPBFT from the shared read service, which brings these same two methods onto the PBFT gRPC service by construction. Aligning the HTTP surface here means HTTP and gRPC expose the same PBFT read set instead of the gap reopening on the other transport.

Key Changes

  • Module: framework only.
    • New: WalletCursorFilter + SolidityCursorFilter / PbftCursorFilter (~60 lines); the @HttpApi / @HttpApiExcluded annotations and the HttpApiRegistry derived from them.
    • Changed: the four HTTP service classes register base servlets (Step 1) and then consume the derived registry (Step 2); the 120 servlets carry their own @HttpApi declaration.
    • Deleted: 96 cursor wrappers, 2 SolidityNode forks, and their orphan tests (~2,900 lines).
  • Configuration: none (http.fullNodePort / solidityPort / PBFTPort and enable switches keep their semantics).
  • API: external paths, ports and response semantics unchanged, except two read-only endpoints are additionally exposed on the PBFT surface (see Compatibility).

Impact

  • Developer Experience: an endpoint is implemented and declared in the same place — adding one is a one-file change instead of edits to four registration lists; surface differences and access nature become reviewable data instead of tribal knowledge.
  • Stability: cursor mechanics are equivalent to the current futureGet (same thread, guaranteed reset). Verified by concurrency tests crossing HEAD × SOLIDITY × PBFT requests and by registry-derived endpoint parity tests.
  • Security: httpApiAccessFilter / liteFnQueryHttpFilter behavior is untouched; the read-only constraint of cursor services and of the solidified-only standalone SolidityNode is upgraded from review convention to a startup-enforced invariant.
  • Performance: one filter invocation replaces one wrapper method call per request — neutral.

Compatibility

  • Breaking Change: No — paths, ports and response formats are unchanged.
  • Default Behavior Change: No, with the following deliberate alignments:
    1. triggerconstantcontract / estimateenergy on the Solidity / PBFT services no longer swallow IOException from response writing (the old wrappers had to catch it inside a lambda); they now behave exactly like the default FullNode service.
    2. The two standalone SolidityNode endpoints gain the base servlets' standard JSON error format and visible=true log address conversion — i.e., existing drift is fixed, not introduced.
    3. The 5 dormant shielded-TRX endpoints are removed from the PBFT service (a confirmed missed deletion from the 2020 sweep; with the enabling committee parameter never activated, they could only return "not supported" errors).
    4. The two endpoints previously served only on the Solidity service — getpaginatednowwitnesslist and gettransactioninfobyblocknum — are now also served on the PBFT surface. The change is additive (existing paths and callers are untouched); both are read-only, general-purpose queries with no surface-specific behavior, so serving them on PBFT closes an accidental gap between the two cursor surfaces. It mirrors the follow-up gRPC merge, which serves the same two methods on the PBFT service by construction.
  • Migration Required: No.

References (Optional)

  • Involved classes: WalletOnCursor, WalletOnSolidity, WalletOnPBFT, Chainbase (ThreadLocal cursor), Manager#setCursor/resetCursor, SnapshotManager.
  • WalletOnSolidity / WalletOnPBFT remain in use by the gRPC services and jsonrpc; they are removed only after the follow-up below.
  • Follow-up (separate issue/PR): the gRPC twins RpcApiServiceOnSolidity / RpcApiServiceOnPBFT (~980 lines of futureGet delegation) can be merged the same way with a ServerInterceptor; gRPC call lifecycle needs its own verification.

Additional Notes

  • Do you have ideas regarding implementation? Yes
  • Are you willing to implement this feature? Yes

Activity

  1. yanghang8612 commented on Aug 20, 2026

    @yanghang8612
    Collaborator

    This proposal looks solid to me. It addresses the duplication in the current HTTP API layer while making endpoint exposure and access constraints much clearer.

    I also noticed that there are many relatively small servlet implementation classes under org.tron.core.services.http. Have you considered organizing and consolidating them by functional domain, such as account and resources, blocks and transactions, contracts, governance, assets and markets, shielded APIs, and node-related APIs?

    This could potentially make the package easier to navigate and maintain. I would be interested to hear your thoughts on whether such a follow-up refactoring would be worthwhile after this work.

  2. SeriousCoding789 commented on Aug 21, 2026

    @SeriousCoding789
    ContributorAuthor

    Thanks @yanghang8612 — agreed, and worth doing. On develop that package holds 133 servlet classes in a single flat directory, so grouping by domain would help a lot.

    Confirming the scope boundary: this proposal removes the duplicated wrapper servlets in the Solidity and PBFT stacks and replaces the hand-written registration lists with a single declarative table — the base servlets in services.http are left untouched. Folding the reorganization into the same change would make it considerably larger and harder to review, so I'd rather keep the two separate.

    Once this work is done, I'll give the reorganization serious consideration as a follow-up, using your domains as the starting taxonomy. Step 2's registry (endpoint → servlet → access nature → surfaces) should also make most of the grouping derivable from the table rather than hand-assigned.

  3. yanghang8612 commented on Aug 21, 2026

    @yanghang8612
    Collaborator

    @Sunny6889 Thanks for the clarification. Keeping the two changes separate makes sense and should make this proposal much easier to review. I’m glad the suggested domains can provide a useful starting point for a follow-up.

  4. halibobo1205 commented on Aug 23, 2026

    @halibobo1205
    Collaborator

    Thanks @Sunny6889 — I agree with the overall direction of both Step 1 and Step 2.

    For the registration-dedup half, I previously explored annotation-based auto-registration in this prototype. It is based on an older codebase and covers registration dedup only — not Step 2's access modeling or safety invariants — so I am linking it as prior art rather than proposing the commit as-is.

    There may be a way to combine the two approaches. HttpApiDef and a sufficiently complete servlet annotation encode the same endpoint metadata — suffix, access, and surfaces — with the servlet type implicit in the annotated class. If both are hand-written, we're back to two sources of truth. Could we instead make the annotation the single declaration source, and derive a read-only registry and audit matrix from it?

    • After the wrappers are removed, each base servlet declares its canonical suffix, access (READ / BUILD / WRITE), and applicable surfaces through one unified @HttpApi annotation; special aliases are declared explicitly.
    • A shared registrar registers routes from this metadata and, before any Jetty bind and regardless of whether the corresponding surface is enabled, validates that each (surface, path) is unique, paths are non-blank, every annotated class is a Spring-managed servlet bean, every concrete servlet under the registry-managed HTTP API package is either explicitly annotated or explicitly opted out, and every endpoint with access != READ declares the FullNode surface and no others.
    • The API × surface × access audit matrix is generated from the same metadata. During migration, parity should still be checked against an independent fixture of the old hand-written routes so that the generated output does not validate itself. Afterwards, a CI-checked generated snapshot would let reviewers see surface changes at a glance.

    This way, annotations provide locality of declaration, while the derived registry provides centralized auditability and boot-time safety, without introducing a second hand-maintained table. One implementation boundary is worth preserving: only directly declared annotations should be accepted, and @HttpApi should never be @Inherited, so Spring's superclass annotation lookup cannot accidentally expose an endpoint.

    Would you consider making the role currently served by the hand-written HttpApiDef enum a derived, read-only registry/view instead?

  5. SeriousCoding789 commented on Aug 24, 2026

    @SeriousCoding789
    ContributorAuthor

    Thanks @Sunny6889 — I agree with the overall direction of both Step 1 and Step 2.

    For the registration-dedup half, I previously explored annotation-based auto-registration in this prototype. It is based on an older codebase and covers registration dedup only — not Step 2's access modeling or safety invariants — so I am linking it as prior art rather than proposing the commit as-is.

    There may be a way to combine the two approaches. HttpApiDef and a sufficiently complete servlet annotation encode the same endpoint metadata — suffix, access, and surfaces — with the servlet type implicit in the annotated class. If both are hand-written, we're back to two sources of truth. Could we instead make the annotation the single declaration source, and derive a read-only registry and audit matrix from it?

    • After the wrappers are removed, each base servlet declares its canonical suffix, access (READ / BUILD / WRITE), and applicable surfaces through one unified @HttpApi annotation; special aliases are declared explicitly.
    • A shared registrar registers routes from this metadata and, before any Jetty bind and regardless of whether the corresponding surface is enabled, validates that each (surface, path) is unique, paths are non-blank, every annotated class is a Spring-managed servlet bean, every concrete servlet under the registry-managed HTTP API package is either explicitly annotated or explicitly opted out, and every endpoint with access != READ declares the FullNode surface and no others.
    • The API × surface × access audit matrix is generated from the same metadata. During migration, parity should still be checked against an independent fixture of the old hand-written routes so that the generated output does not validate itself. Afterwards, a CI-checked generated snapshot would let reviewers see surface changes at a glance.

    This way, annotations provide locality of declaration, while the derived registry provides centralized auditability and boot-time safety, without introducing a second hand-maintained table. One implementation boundary is worth preserving: only directly declared annotations should be accepted, and @HttpApi should never be @Inherited, so Spring's superclass annotation lookup cannot accidentally expose an endpoint.

    Would you consider making the role currently served by the hand-written HttpApiDef enum a derived, read-only registry/view instead?

    Thanks @halibobo1205 — I'd like to take exactly this approach. Plan: make HttpApiDef's role a read-only registry derived from a single @httpapi(value, access, surfaces) declaration on each servlet, with the audit matrix generated from the same source. HttpApiRegistry would build and validate the whole table at class-load, before any Jetty bind, so an invalid table fails the boot rather than mounting.

    Both of your implementation notes would be enforced (and pinned by tests): @httpapi never @inherited and read only via getDeclaredAnnotation, and every concrete servlet under the package must declare either @httpapi or an explicit @HttpApiExcluded — so a servlet can't be added and silently left unmounted. The API × surface × access matrix would be generated and checked against a committed snapshot, and — per your point about not letting the output validate itself — I'd keep a separate fixture of the pre-refactor hand-written routes as the independent parity baseline.

    That baseline looks worth having: a local prototype of it already surfaced that the hand-written table drops five shielded read endpoints still live on the PBFT surface (getmerkletreevoucherinfo, scanandmarknotebyivk, scannotebyivk, scannotebyovk, isspend), which the derived-plus-baseline setup would catch and preserve. I'll fold this into the PR. Thanks for pushing on it.

  6. lxcmyf commented on Aug 26, 2026

    @lxcmyf
    Collaborator

    The annotation-derived registry direction looks good to me and resolves the two-sources-of-truth concern. Before implementation, I think two points need to be made explicit:

    1. The latest reply says the independent old-route fixture should catch and preserve the five PBFT shielded endpoints. That conflicts with the issue body (including Compatibility item 3), which describes them as a missed 2020 deletion and says they will be removed. Current upstream/develop confirms that these routes are still active only in HttpApiOnPBFTService, while the corresponding FullNode registrations are commented out. The parity fixture therefore needs to distinguish exact parity from reviewed, intentional deltas—for example, an explicit expected-diff/allowlist pinned by tests. Please choose remove or preserve and make the proposal, fixture, and generated matrix agree.

    2. “Build and validate the whole table at class-load” is too early for the check that every annotated servlet is a Spring-managed bean. Metadata-only invariants (direct annotation, unique (surface, path), non-blank paths, access/surface rules) can be checked statically, but bean resolution must happen after the ApplicationContext is ready and before HttpService.start() binds Jetty. I suggest specifying two validation phases and adding a startup-order test proving that a context-validation failure leaves no HTTP port bound.

    With those clarified, I support making the annotation the declaration source and the registry a derived, read-only audit view.

  7. SeriousCoding789 commented on Aug 27, 2026

    @SeriousCoding789
    ContributorAuthor

    The annotation-derived registry direction looks good to me and resolves the two-sources-of-truth concern. Before implementation, I think two points need to be made explicit:

    1. The latest reply says the independent old-route fixture should catch and preserve the five PBFT shielded endpoints. That conflicts with the issue body (including Compatibility item 3), which describes them as a missed 2020 deletion and says they will be removed. Current upstream/develop confirms that these routes are still active only in HttpApiOnPBFTService, while the corresponding FullNode registrations are commented out. The parity fixture therefore needs to distinguish exact parity from reviewed, intentional deltas—for example, an explicit expected-diff/allowlist pinned by tests. Please choose remove or preserve and make the proposal, fixture, and generated matrix agree.
    2. “Build and validate the whole table at class-load” is too early for the check that every annotated servlet is a Spring-managed bean. Metadata-only invariants (direct annotation, unique (surface, path), non-blank paths, access/surface rules) can be checked statically, but bean resolution must happen after the ApplicationContext is ready and before HttpService.start() binds Jetty. I suggest specifying two validation phases and adding a startup-order test proving that a context-validation failure leaves no HTTP port bound.

    With those clarified, I support making the annotation the declaration source and the registry a derived, read-only audit view.

    Thanks @lxcmyf — both worth pinning down.

    1. Remove, not preserve. The issue body is correct and my earlier reply was not; sorry for the confusion. The five shielded endpoints are removed from the PBFT surface, aligning it with the other three where they have been disabled since 2020.

    2. Two phases — agreed, and the wording is what misleads. What the registry validates on first touch is metadata only: directly-declared annotations, unique (surface, path), non-blank path tokens, access/surface rules. The @Component check belongs to that set — it inspects the annotation, it does not resolve a bean, and the registry never touches the ApplicationContext.

    Bean resolution is separate and already later: servlets are resolved via appContext.getBean(...) while the service mounts its endpoints, after the context is ready and before Jetty binds — HttpService.start() runs addServlet(...) first, and the listener is only opened by the subsequent apiServer.start(). So a failure in either phase already leaves no port bound. But that ordering is implicit today, so I'll state the two phases explicitly and add the startup-order test you suggest.

  8. lxcmyf commented on Sep 2, 2026

    @lxcmyf
    Collaborator

    Thanks, that clarifies both points. Please update the issue body to reflect the removal decision and the two validation phases.

    One compatibility issue still needs to be addressed: RateLimiterServlet registers and looks up limiters using getClass().getSimpleName(). Today the FullNode, Solidity, and PBFT wrapper classes therefore have different limiter keys and independent limiter instances. After removing the wrappers, these surfaces will use the same base servlet class/key, so existing *OnSolidityServlet / *OnPBFTServlet configurations may be silently ignored, and traffic on different ports may share one limiter.

    Please clarify whether the existing per-surface behavior will be preserved or treated as a configuration migration, and add coverage for it.

  9. linked a pull request that will close this issuefeat(API): refactor merge http servlets #6976on Sep 18, 2026
  10. removed this from the GreatVoyage-v4.8.3 milestone on Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    topic:apirpc/http related issue

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions