lysine-dev/retrofit · error · IllegalStateException

Response must be parameterized as Response<Foo> or Response<

Error message

Response must be parameterized as Response<Foo> or Response<? extends Foo>

What it means

Thrown by ScalaCallAdapterFactory.get() when the method returns Future<Response> with a raw inner Response. The factory resolved the outer Future's type argument (innerType) and found it is Response.class, so it now requires innerType itself to be a ParameterizedType so it can extract the concrete body type to deserialize. A raw Response yields no body type.

Source

Thrown at retrofit-adapters/scala/src/main/java/retrofit2/adapter/scala/ScalaCallAdapterFactory.java:76

  @Override
  public @Nullable CallAdapter<?, ?> get(
      Type returnType, Annotation[] annotations, Retrofit retrofit) {
    if (getRawType(returnType) != Future.class) {
      return null;
    }
    if (!(returnType instanceof ParameterizedType)) {
      throw new IllegalStateException(
          "Future return type must be parameterized as Future<Foo> or Future<? extends Foo>");
    }
    Type innerType = getParameterUpperBound(0, (ParameterizedType) returnType);

    if (getRawType(innerType) != Response.class) {
      // Generic type is not Response<T>. Use it for body-only adapter.
      return new BodyCallAdapter<>(innerType);
    }

    if (!(innerType instanceof ParameterizedType)) {
      throw new IllegalStateException(
          "Response must be parameterized as Response<Foo> or Response<? extends Foo>");
    }

    Type responseType = getParameterUpperBound(0, (ParameterizedType) innerType);
    return new ResponseCallAdapter<>(responseType);
  }
}

View on GitHub (pinned to d0b112dad0)

Solutions

  1. Parameterize the inner Response: change Future<Response> to Future<Response<User>>.
  2. If you only need the deserialized body, drop Response entirely and return Future<User>.
  3. Recompile so the corrected signature is re-evaluated when Retrofit builds the call adapter.

Example fix

// before
@GET("users/{id}")
Future<Response> getUser(@Path("id") String id);

// after
@GET("users/{id}")
Future<Response<User>> getUser(@Path("id") String id);
Defensive patterns

Strategy: validation

Validate before calling

// Signature discipline; no per-call pre-check. Catch at Retrofit.create() in a unit test.
// Static check that an outer Future<Response> carries a parameterized Response:
Method m = ApiService.class.getMethod("getUser", String.class);
Type rt = m.getGenericReturnType();
if (rt instanceof ParameterizedType) {
  Type inner = ((ParameterizedType) rt).getActualTypeArguments()[0];
  if (inner == retrofit2.Response.class) {
    throw new AssertionError("getUser() returns Future<Response>; parameterize the Response.");
  }
}

Type guard

// Enforce in Scala/Java by always declaring the inner type explicitly:
//   Future<Response<User>> rather than Future<Response>.

Try / catch

try {
  ApiService api = retrofit.create(ApiService.class);
} catch (IllegalStateException e) {
  if (e.getMessage() != null && e.getMessage().contains("Response must be parameterized")) {
    throw new ConfigurationException("Service interface has Future<Response> with raw Response", e);
  }
  throw e;
}

Prevention

When it happens

Trigger: A Retrofit interface method declared as Future<Response> getUser(...) — the outer Future is parameterized but its argument is the raw retrofit2.Response class. After the `getRawType(innerType) != Response.class` check passes, the `innerType instanceof ParameterizedType` check fails and throws.

Common situations: Wanting access to headers/status code via Response but forgetting the body generic, or converting a Future<User> method to Future<Response> during a refactor and stopping one level short.

Related errors


AI-assisted analysis of lysine-dev/retrofit@d0b112dad0 (2026-08-13). Data as JSON: /api/errors/b6cad7296bf72864. Report an issue: GitHub.