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

For Java8-adapter methods declared as CompletableFuture<Response<...>>, the nested Response must be generic so the adapter can extract the body type. A raw Response (CompletableFuture<Response>) yields no deserialization target, so Java8CallAdapterFactory throws IllegalStateException when the inner type is not a ParameterizedType.

Source

Thrown at retrofit-adapters/java8/src/main/java/retrofit2/adapter/java8/Java8CallAdapterFactory.java:80

      Type returnType, Annotation[] annotations, Retrofit retrofit) {
    if (getRawType(returnType) != CompletableFuture.class) {
      return null;
    }
    if (!(returnType instanceof ParameterizedType)) {
      throw new IllegalStateException(
          "CompletableFuture return type must be parameterized"
              + " as CompletableFuture<Foo> or CompletableFuture<? 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);
    }

    // Generic type is Response<T>. Extract T and create the Response version of the adapter.
    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);
  }

  private static final class BodyCallAdapter<R> implements CallAdapter<R, CompletableFuture<R>> {
    private final Type responseType;

    BodyCallAdapter(Type responseType) {
      this.responseType = responseType;
    }

    @Override
    public Type responseType() {
      return responseType;
    }

View on GitHub (pinned to d0b112dad0)

Solutions

  1. Parameterize Response: `CompletableFuture<Response<User>> getUser(@Path("id") long id);`
  2. Drop Response if you only need the body: `CompletableFuture<User> getUser(@Path("id") long id);`

Example fix

// before
CompletableFuture<Response> getUser(@Path("id") long id);
// after
CompletableFuture<Response<User>> getUser(@Path("id") long id);
Defensive patterns

Strategy: validation

Validate before calling

import java.lang.reflect.*;
static void assertNestedResponseParameterized(Class<?> iface) {
  for (Method m : iface.getDeclaredMethods()) {
    if (!(m.getGenericReturnType() instanceof ParameterizedType)) continue;
    Type inner = ((ParameterizedType) m.getGenericReturnType()).getActualTypeArguments()[0];
    if (inner.getTypeName().startsWith("retrofit2.Response") && !(inner instanceof ParameterizedType)) {
      throw new AssertionError("Raw nested Response on " + m);
    }
  }
}

Try / catch

try {
  CompletableFuture<Response<User>> f = service.getUser(id);
} catch (IllegalStateException e) {
  throw new IllegalStateException("Nested Response return type is raw: " + e.getMessage(), e);
}

Prevention

When it happens

Trigger: A method like `CompletableFuture<Response> getUser();` — outer CompletableFuture is parameterized but the inner Response is raw. Thrown in get() after confirming the inner raw type is Response.class.

Common situations: Adding the Response wrapper for status/headers and forgetting the body generic; partial refactor.

Related errors


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