Repository navigation
Kotlin/Java: Retrofit client interfaces are minted as server Route nodes instead of outbound HTTP_CALLS #2723
Description
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_CALLSedge, socross-repo-intelligencereports
cross_http_calls: 0for 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 aRouteidentical to the server's real one.cross-repo-intelligencefrom any other project that
calls the same endpoint then emits aCROSS_HTTP_CALLSpair 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 thebaseUrl
(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.
- the idiomatic relative path (
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
HANDLESedges onto 67 clientRoutenodes, 0HTTP_CALLS; - 16 methods (relative paths and
const valtemplates) collapse onto__route__GET__/and
__route__POST__/; - of the 65 other distinct paths, 55 match a
HANDLES-backedRouteof 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_CALLSfrom 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
DeusData commentedon Oct 25, 2026
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.