Skip to content

Kotlin/Java: Retrofit client interfaces are minted as server Route nodes instead of outbound HTTP_CALLS #2723

Open

Description

@emouty

Version

codebase-memory-mcp 0.12.1

Platform

Linux (x64)

Install channel

GitHub release archive / install.sh / install.ps1

Binary variant

ui

What happened, and what did you expect?

A Retrofit service interface (@GET/@POST/... from retrofit2.http, in Kotlin or Java) declares the HTTP calls
a client makes, but it is indexed as if the client served them: each annotated method becomes a Route
node of the client project with a HANDLES edge from the interface method. Three consequences:

  • Outbound calls lost. The interface methods get no HTTP_CALLS edge, so cross-repo-intelligence reports
    cross_http_calls: 0 for a client whose whole HTTP surface is Retrofit, which is the usual shape of an
    Android app.
  • Other callers get linked to the client. A path written with a leading slash (@GET("/v1/orders/{id}"),
    which Retrofit treats as absolute: "Endpoint values which contain a leading / are absolute",
    Retrofit.java:581)
    yields a Route identical to the server's real one. cross-repo-intelligence from any other project that
    calls the same endpoint then emits a CROSS_HTTP_CALLS pair into the client as well as into the server, as if
    the client's interface method handled the request.
  • Paths collapse onto /. Any path that is not a string literal with a leading slash becomes /, so all
    such methods collide on __route__GET__/ / __route__POST__/:
    • the idiomatic relative path (@POST("v1/orders")), resolved at runtime against the baseUrl
      (Retrofit.java:560);
    • a Kotlin const val, alone (@GET(ORDERS)) or in a string template (@GET("$ORDERS/{id}/status")), even
      when the constant itself starts with /. Retrofit annotation arguments must be compile-time constants, so
      this is a common way to share a path prefix across the methods of one interface.

Expected: see "Result vs expected" in the Reproduction section.

Reproduction

1. Code. Three dummy repos, created as /tmp/cbm-kotlin-retrofit-client, /tmp/cbm-orders-server and
/tmp/cbm-billing-caller.

Client repo /tmp/cbm-kotlin-retrofit-client, under src/main/kotlin/com/acme/client/:

// OrderApi.kt   (Retrofit, Kotlin coroutines)
package com.acme.client

import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.Path

private const val ORDERS = "/v1/orders"

interface OrderApi {
    @GET("/v1/orders/{id}")
    suspend fun getOrder(@Path("id") id: String): String

    @POST("v1/orders")
    suspend fun createOrder(@Body body: String): String

    @GET(ORDERS)
    suspend fun listOrders(): String

    @GET("$ORDERS/{id}/status")
    suspend fun getOrderStatus(@Path("id") id: String): String
}

Server repo /tmp/cbm-orders-server, under src/main/java/com/acme/server/ (JAX-RS; indexes correctly):

// OrderResource.java
package com.acme.server;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
@Path("/v1/orders")
public class OrderResource {
    @GET public String list() { return "[]"; }
    @GET @Path("/{id}") public String get(@PathParam("id") String id) { return id; }
    @GET @Path("/{id}/status") public String status(@PathParam("id") String id) { return id; }
    @POST public String create(String body) { return body; }
}

Another caller of the same server, /tmp/cbm-billing-caller, under src/main/java/com/acme/billing/ (an inline
RestTemplate, a shape that already produces HTTP_CALLS):

// BillingJob.java
package com.acme.billing;
import org.springframework.web.client.RestTemplate;
public class BillingJob {
    public String order(String id) {
        return new RestTemplate().getForObject("/v1/orders/{id}", String.class, id);
    }
}

2. Commands. The project ids below are what the indexer derives from the /tmp/... paths.

codebase-memory-mcp cli index_repository '{"repo_path":"/tmp/cbm-kotlin-retrofit-client","mode":"moderate"}'
codebase-memory-mcp cli index_repository '{"repo_path":"/tmp/cbm-orders-server","mode":"moderate"}'
codebase-memory-mcp cli index_repository '{"repo_path":"/tmp/cbm-billing-caller","mode":"moderate"}'
codebase-memory-mcp cli query_graph '{"project":"tmp-cbm-kotlin-retrofit-client","query":"MATCH (f)-[e:HANDLES]->(r:Route) RETURN f.file_path, f.name, r.qualified_name"}'
codebase-memory-mcp cli query_graph '{"project":"tmp-cbm-kotlin-retrofit-client","query":"MATCH (f)-[e:HTTP_CALLS]->(r) RETURN f.name, r.qualified_name"}'
codebase-memory-mcp cli query_graph '{"project":"tmp-cbm-orders-server","query":"MATCH (r:Route) RETURN r.qualified_name"}'
codebase-memory-mcp cli index_repository '{"repo_path":"/tmp/cbm-kotlin-retrofit-client","mode":"cross-repo-intelligence","target_projects":["tmp-cbm-orders-server"]}'
codebase-memory-mcp cli index_repository '{"repo_path":"/tmp/cbm-billing-caller","mode":"cross-repo-intelligence","target_projects":["tmp-cbm-kotlin-retrofit-client","tmp-cbm-orders-server"]}'
codebase-memory-mcp cli query_graph '{"project":"tmp-cbm-kotlin-retrofit-client","query":"MATCH (h)-[e:CROSS_HTTP_CALLS]->(r) RETURN h.file_path, h.name, r.qualified_name"}'
codebase-memory-mcp cli query_graph '{"project":"tmp-cbm-orders-server","query":"MATCH (h)-[e:CROSS_HTTP_CALLS]->(r) RETURN h.file_path, h.name, r.qualified_name"}'

3. Result vs expected. CLI output, trimmed to the rows.

Client HANDLES edges (expected: 0 rows; only the leading-slash literal keeps its path, the relative path and
both const val forms collapse to /):

rows: 4  (cols: f.file_path f.name r.qualified_name)
  src/main/kotlin/com/acme/client/OrderApi.kt getOrder __route__GET__/v1/orders/{}
  src/main/kotlin/com/acme/client/OrderApi.kt createOrder __route__POST__/
  src/main/kotlin/com/acme/client/OrderApi.kt listOrders __route__GET__/
  src/main/kotlin/com/acme/client/OrderApi.kt getOrderStatus __route__GET__/

Client HTTP_CALLS edges:

rows: 0  (cols: f.name r.qualified_name)

Expected 4 rows:

  getOrder       __route__GET__/v1/orders/{}
  createOrder    __route__POST__/v1/orders
  listOrders     __route__GET__/v1/orders
  getOrderStatus __route__GET__/v1/orders/{}/status

Server Route nodes (correct):

rows: 4  (cols: r.qualified_name)
  __route__GET__/v1/orders
  __route__GET__/v1/orders/{}
  __route__GET__/v1/orders/{}/status
  __route__POST__/v1/orders

cross-repo-intelligence from the client: "cross_http_calls":0. Expected 4.

cross-repo-intelligence from the billing caller: "cross_http_calls":2 for its single call. Expected 1. The
reverse edges show the second one landing on the client's interface method:

# tmp-cbm-kotlin-retrofit-client (expected: 0 rows)
rows: 1  (cols: h.file_path h.name r.qualified_name)
  src/main/kotlin/com/acme/client/OrderApi.kt getOrder __route__GET__/v1/orders/{}

# tmp-cbm-orders-server (correct)
rows: 1  (cols: h.file_path h.name r.qualified_name)
  src/main/java/com/acme/server/OrderResource.java get __route__GET__/v1/orders/{}

The same holds for a Java Retrofit interface: @GET("v1/orders/{id}") and @POST("v1/orders") on a Java
interface give HANDLES to __route__GET__/ and __route__POST__/ and no HTTP_CALLS.

Related: #2668 (Java outbound calls through injected clients, format strings and request builders yield no
HTTP_CALLS; same "outbound call lost" family, different mechanism), #1248 (false Routes minted from
non-registration string literals).

Logs


Diagnostics trajectory (memory / performance / leak issues)


Project scale (if relevant)

Impact on a real codebase. On a production Kotlin Android app (29 Retrofit interfaces, 85 annotated
methods; proprietary, so only counts here):

  • 85 HANDLES edges onto 67 client Route nodes, 0 HTTP_CALLS;
  • 16 methods (relative paths and const val templates) collapse onto __route__GET__/ and
    __route__POST__/;
  • of the 65 other distinct paths, 55 match a HANDLES-backed Route of the backend services the app talks to,
    so classifying them as outbound calls would give the app 55 correct cross-repo links instead of 0 (the
    remaining 10 are static files and third-party APIs);
  • 15 CROSS_HTTP_CALLS from call sites in 6 other repositories of the same fleet land on the app's interface
    methods.

Confirmations

  • I searched existing issues and this is not a duplicate.
  • My reproduction uses shareable code (a dummy snippet or a public OSS repository), not proprietary code.

Activity

  1. added
    parsing/qualityGraph extraction bugs, false positives, missing edges
    ux/behaviorDisplay bugs, docs, adoption UX
    windowsWindows-specific issues
    on Oct 20, 2026
  2. DeusData commented on Oct 25, 2026

    @DeusData
    Owner

    Thank you for the three-project fixture and for distinguishing client declarations from server handlers. The wrong-direction links and path collapse are both clearly illustrated. Sorry for the delay; this is waiting on maintainer investigation, and I’ll follow up soon.

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

    bugSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgesux/behaviorDisplay bugs, docs, adoption UX

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions