Jesse A. Green / notebook

Retrofit User Guide

Generated June 2026 from square/retrofit at 8e83a24

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

Retrofit turns your HTTP API into a type-safe Java or Kotlin interface. You describe each endpoint with an annotated method, and Retrofit generates a working client that builds requests, executes them over OkHttp, and deserializes responses into your own objects. This guide shows you how to use every part of that surface — from your first request to multipart uploads, streaming downloads, RxJava and coroutine integration, and testing.

This guide covers Retrofit 3.0.0 (Maven coordinates com.squareup.retrofit2:retrofit:3.0.0). Written from the library source.


1. Quick Start

Define an interface that describes the API, ask Retrofit to implement it, then call a method.

// 1. Describe the endpoint.
public interface GitHubService {
  @GET("users/{user}/repos")
  Call<List<Repo>> listRepos(@Path("user") String user);
}

// 2. Build a Retrofit instance and create the implementation.
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build();

GitHubService service = retrofit.create(GitHubService.class);

// 3. Make a call.
Call<List<Repo>> repos = service.listRepos("octocat");
List<Repo> result = repos.execute().body();

The Retrofit class generates an implementation of GitHubService at runtime. Each Call returned by the service can make a synchronous or asynchronous HTTP request to the remote server.

Note

Retrofit only deserializes response bodies into OkHttp's ResponseBody type out of the box. To map a response onto your own class (such as List<Repo> above), you must add a converter like Gson. See §5 Converting bodies.

Tip

Reuse a single Retrofit instance (and the single OkHttpClient behind it) across your whole application. They are expensive to create and hold the connection pool and thread pools that make repeated requests fast.


2. Installation

Retrofit requires at minimum Java 8+ or Android API 21+.

Add the dependency

Gradle (Kotlin DSL)

implementation("com.squareup.retrofit2:retrofit:3.0.0")

Gradle (Groovy DSL)

implementation 'com.squareup.retrofit2:retrofit:3.0.0'

Maven

<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>retrofit</artifactId>
  <version>3.0.0</version>
</dependency>

Retrofit depends on OkHttp and pulls it in transitively — you do not need to declare OkHttp yourself unless you want to pin a specific version. As of Retrofit 3.0, OkHttp is the Kotlin-based 4.12 release, so a small transitive Kotlin runtime dependency comes along with it.

Note

Retrofit 3.x is forward binary-compatible with 2.x. Libraries compiled against Retrofit 2.x can still run against 3.x.

Add a converter (almost always needed)

Pick the converter that matches your API's wire format and add it alongside the core dependency. For JSON with Gson:

Gradle (Kotlin DSL)

implementation("com.squareup.retrofit2:converter-gson:3.0.0")

Maven

<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>converter-gson</artifactId>
  <version>3.0.0</version>
</dependency>

The full list of converters and adapters is in §5 and §7.

Keep your versions aligned with the BOM (optional)

If you use several Retrofit modules (core + converters + adapters), import the bill of materials so every module stays on the same version:

implementation(platform("com.squareup.retrofit2:retrofit-bom:3.0.0"))
implementation("com.squareup.retrofit2:retrofit")
implementation("com.squareup.retrofit2:converter-moshi")
implementation("com.squareup.retrofit2:adapter-rxjava3")

With the BOM in place you omit version numbers on the individual modules.

Tip

Want the bleeding edge? In-development snapshots are published to the Central Portal Snapshots repository at https://central.sonatype.com/repository/maven-snapshots/. Add that as a repository to use a -SNAPSHOT build.


3. Defining Your API Interface

An API in Retrofit is a plain Java (or Kotlin) interface. Every method maps to one HTTP request, and annotations on the method and its parameters describe exactly how that request is shaped.

public interface GitHubService {
  @GET("/repos/{owner}/{repo}/contributors")
  Call<List<Contributor>> contributors(
      @Path("owner") String owner,
      @Path("repo") String repo);
}

Important

API declarations must be interfaces — Retrofit throws IllegalArgumentException: API declarations must be interfaces. if you pass a class. Interfaces also may not declare type parameters (generics on the interface itself are rejected).

You can spread methods across multiple interfaces (one per resource, for example) and create each with its own retrofit.create(...) call against the same Retrofit instance.


4. Request Declarations: The Annotation Reference

This is the heart of Retrofit. Every annotation below lives in the retrofit2.http package. They fall into five groups: request method, URL & parameters, headers, request body, and form / multipart.

4.1 Request method annotations

Every service method must carry exactly one HTTP method annotation. There are eight built in:

Annotation HTTP verb Has request body?
@GET GET No
@POST POST Yes
@PUT PUT Yes
@PATCH PATCH Yes
@DELETE DELETE No (use @HTTP for a body)
@HEAD HEAD No
@OPTIONS OPTIONS No
@HTTP custom Configurable

Each verb annotation takes the endpoint path as its value:

@GET("users/list")
Call<List<User>> userList();

You can include static query parameters directly in the path:

@GET("users/list?sort=desc")
Call<List<User>> sortedUserList();

Custom verbs and DELETE with a body — @HTTP

@HTTP lets you use a non-standard method, set the path, and declare whether the request carries a body:

public interface Service {
  @HTTP(method = "CUSTOM", path = "custom/endpoint/")
  Call<ResponseBody> customEndpoint();

  // DELETE with a request body (which @DELETE alone cannot express):
  @HTTP(method = "DELETE", path = "remove/", hasBody = true)
  Call<ResponseBody> deleteObject(@Body RequestBody object);
}
@HTTP element Meaning Default
method The HTTP verb to send (required) —
path Relative/absolute path or full URL ""
hasBody Whether the request sends a body false

4.2 URL manipulation

@Path — fill in URL segments

A replacement block is an alphanumeric string wrapped in { and }. A parameter annotated with @Path using the same name fills it in. Values are converted to strings and URL-encoded.

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId);

Calling groupList(42) requests group/42/users.

By default the value is URL-encoded. Use encoded = true if your argument is already encoded:

@GET("/user/{name}")
Call<ResponseBody> notEncoded(@Path(value = "name", encoded = true) String name);

encoded("John%Doe") yields /user/John%25Doe; notEncoded("John%Doe") yields /user/John%Doe.

Caution

@Path parameters may not be null. Passing null throws an exception when the request is built.

@Query — append query parameters

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);

@Query values are URL-encoded and appended to the URL. null values are ignored (the parameter is omitted). Passing a List or array produces one query parameter per non-null item:

@GET("/friends")
Call<ResponseBody> friends(@Query("group") String... groups);

friends("coworker", "bowling") yields /friends?group=coworker&group=bowling. Use @Query(value = "group", encoded = true) to skip encoding.

@QueryMap — many query parameters at once

@GET("group/{id}/users")
Call<List<User>> groupList(@Path("id") int groupId, @QueryMap Map<String, String> options);

Every map entry becomes a query parameter. With @QueryMap the map itself, its keys, and its values may not be null.

@QueryName — value-less query parameters

For query parameters that are just a name with no =value:

@GET("/friends")
Call<ResponseBody> friends(@QueryName String filter);

friends("contains(Bob)") yields /friends?contains(Bob). Lists and arrays produce one entry each.

@Url — fully dynamic URLs

When the whole URL is computed at runtime, omit the path on the verb annotation and annotate the first parameter with @Url:

@GET
Call<ResponseBody> list(@Url String url);

The @Url value is resolved against the base URL using the same rules as a relative link in a web page (see §6.1).

4.3 Header manipulation

@Headers — static headers on a method

@Headers("Cache-Control: max-age=640000")
@GET("widget/list")
Call<List<Widget>> widgetList();

@Headers({
  "Accept: application/vnd.github.v3.full+json",
  "User-Agent: Retrofit-Sample-App"
})
@GET("users/{username}")
Call<User> getUser(@Path("username") String username);

@Header — a header from a parameter

@GET("user")
Call<User> getUser(@Header("Authorization") String authorization);

If the value is null, the header is omitted. Otherwise toString() is called on the value. A List or array produces one header per non-null item.

@HeaderMap — many headers from a map

@GET("user")
Call<User> getUser(@HeaderMap Map<String, String> headers);

Warning

Headers do not overwrite each other. Every header with the same name is included in the request. If you need replacement semantics, manage it yourself.

Note

Header parameter keys and values allow only ASCII by default. To send non-ASCII values (not strictly HTTP-compliant but often required), set allowUnsafeNonAsciiValues = true on @Header, @Headers, or @HeaderMap:

@GET("/")
Call<ResponseBody> foo(@Header(value = "Accept-Language", allowUnsafeNonAsciiValues = true) String lang);

Headers that must be added to every request are best set with an OkHttp interceptor rather than repeated on each method (see §6.2).

4.4 The request body — @Body

Send any object as the request body with @Body. It is serialized by the Retrofit instance's converter.

@POST("users/new")
Call<User> createUser(@Body User user);

Important

If no converter is installed, only OkHttp's RequestBody type can be used as a @Body. @Body parameters may not be null.

4.5 Form-encoded requests

Add @FormUrlEncoded to the method and annotate each pair with @Field. The request is sent with application/x-www-form-urlencoded.

@FormUrlEncoded
@POST("user/edit")
Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last);

updateUser("Bob Smith", "President") sends the body first_name=Bob+Smith&last_name=President. null field values are ignored; passing a List or array produces a field pair per item.

For a variable set of fields, use @FieldMap:

@FormUrlEncoded
@POST("/things")
Call<ResponseBody> things(@FieldMap Map<String, String> fields);

Both @Field and @FieldMap accept encoded = true if the names/values are already URL-encoded. A null map, key, or value is not allowed for @FieldMap.

4.6 Multipart requests and file upload

Add @Multipart to the method and declare each part with @Part.

@Multipart
@PUT("user/photo")
Call<User> updateUser(
    @Part("photo") RequestBody photo,
    @Part("description") RequestBody description);

A @Part parameter is processed one of three ways depending on its type:

Parameter type Behavior Name in annotation
okhttp3.MultipartBody.Part Used directly Omit the name: @Part MultipartBody.Part part
okhttp3.RequestBody Used directly with its content type Supply the name: @Part("foo") RequestBody foo
Any other type Serialized by a converter Supply the name: @Part("foo") Image photo
@Multipart
@POST("/")
Call<ResponseBody> example(
    @Part("description") String description,
    @Part(value = "image", encoding = "8-bit") RequestBody image);

The encoding element controls the part's Content-Transfer-Encoding (default "binary"). For a dynamic set of parts use @PartMap Map<String, RequestBody>. @Part values may be null (the part is omitted); a @PartMap map, key, or value may not be null.

4.7 Other parameter annotations

@Streaming — stream large response bodies

By default Retrofit buffers the entire response body into memory before handing it to you. For large downloads, add @Streaming so the ResponseBody is delivered as-is and you read it incrementally.

@Streaming
@GET("download/{id}")
Call<ResponseBody> download(@Path("id") String id);

Caution

With @Streaming you are responsible for closing the ResponseBody (e.g. with try-with-resources). The body must be consumed and closed on the calling thread.

@Tag — attach metadata for interceptors

@Tag attaches an object to the underlying OkHttp request using its type as the key. Interceptors can read it back to make per-request decisions (logging, metrics, auth scoping).

@GET("/")
Call<ResponseBody> foo(@Tag String tag);

null tags are omitted. A parameterized type uses its raw type as the key (List<String> → List.class); primitives are boxed (long → Long.class). Duplicate tag types on one method are not allowed.

4.8 Annotation cheat sheet

Annotation Target Purpose
@GET @POST @PUT @PATCH @DELETE @HEAD @OPTIONS method Standard HTTP verbs
@HTTP method Custom verb / DELETE with body
@Path parameter Fill a {block} in the URL
@Query parameter Append ?name=value
@QueryMap parameter Append many query parameters
@QueryName parameter Append a value-less query parameter
@Url parameter Provide the whole URL at runtime
@Header parameter Set one header from an argument
@HeaderMap parameter Set many headers from a map
@Headers method Static headers
@Body parameter Object → request body via converter
@FormUrlEncoded method Send application/x-www-form-urlencoded
@Field / @FieldMap parameter Form fields
@Multipart method Send a multipart body
@Part / @PartMap parameter Multipart parts
@Streaming method Don't buffer the response body
@Tag parameter Attach a typed tag for interceptors

5. Converting Bodies (JSON, XML, and more)

By default Retrofit can only read OkHttp ResponseBody and write RequestBody. Converters teach it to map your own types onto JSON, XML, protocol buffers, and so on. Add them with addConverterFactory(...):

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build();

5.1 Built-in converters

Library Maven artifact Notes
Gson converter-gson JSON
Jackson converter-jackson JSON, XML, CBOR, YAML, etc. by supplying a different mapper + media type
Moshi converter-moshi JSON
Protobuf converter-protobuf Protocol buffers
Wire converter-wire Protocol buffers (Wire runtime)
Simple XML converter-simplexml XML
JAXB converter-jaxb XML
JAXB 3 converter-jaxb3 XML (Jakarta jakarta.xml.bind)
Kotlin serialization converter-kotlinx-serialization Multiformat
Scalars converter-scalars String + primitives and their boxed types

Tip

The scalars converter (converter-scalars) handles plain String, int, boolean, and other primitives as request/response bodies. Add it when an endpoint returns or accepts a bare value rather than a JSON document.

5.2 Delegating (Optional) converters

These do not parse bytes themselves — they delegate to another converter and wrap a potentially-null result in an Optional:

Wrapper type Maven artifact
Guava Optional<T> converter-guava
Java 8 Optional<T> converter-java8

Install the delegating converter before the real one so it wraps that converter's output.

5.3 Mixing converters

You can add more than one converter. Retrofit tries each in the order added until one handles the type. A common pattern is JSON for most endpoints plus the scalars converter for raw text:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl(API_URL)
    .addConverterFactory(ScalarsConverterFactory.create())
    .addConverterFactory(GsonConverterFactory.create())
    .build();

5.4 Custom converters

To support a format Retrofit doesn't ship (YAML, a bespoke text format, or a different library for an existing format), extend Converter.Factory and install an instance. The factory implements any of three methods — responseBodyConverter, requestBodyConverter, stringConverter — returning null for types it doesn't handle. See the Admin Guide for the full SPI contract.


6. Configuring Retrofit

The Retrofit.Builder is where you assemble an instance. baseUrl(...) is the only required call.

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.example.com/")          // required
    .client(myOkHttpClient)                         // optional: custom HTTP client
    .addConverterFactory(MoshiConverterFactory.create())
    .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
    .validateEagerly(BuildConfig.DEBUG)             // optional: fail fast on bad declarations
    .build();
Builder method Purpose
baseUrl(String / HttpUrl / URL) The API base URL (required)
client(OkHttpClient) Use a configured OkHttp client (convenience for callFactory)
callFactory(okhttp3.Call.Factory) Supply a custom call factory; client(...) sets this automatically
addConverterFactory(Converter.Factory) Add a body/string converter
addCallAdapterFactory(CallAdapter.Factory) Support return types other than Call
callbackExecutor(Executor) Thread on which Callback methods run (for Call returns)
validateEagerly(boolean) Validate every method at create(...) time instead of on first call
build() Produce the Retrofit instance

Tip

Already have a Retrofit and want a variation? Call retrofit.newBuilder() to get a builder pre-populated with the existing configuration, change what you need, and build() a new instance that shares the same connection pool.

6.1 Base URL rules

The endpoint values on your annotations are resolved against the base URL exactly the way an <a href=""> link resolves on a web page.

Important

Base URLs should always end in /. Retrofit enforces this — baseUrl(...) throws IllegalArgumentException: baseUrl must end in / otherwise. A trailing / ensures relative endpoint paths append correctly.

Base URL Endpoint Result
http://example.com/api/ foo/bar/ http://example.com/api/foo/bar/
http://example.com/api/ (no /: …/api) foo/bar/ http://example.com/foo/bar/ ⚠️
http://example.com/api/ /foo/bar/ (leading /) http://example.com/foo/bar/ (absolute)
http://example.com/ https://github.com/square/retrofit/ https://github.com/square/retrofit/ (full URL wins)

In short: a leading / on the endpoint makes it absolute (host kept, path replaced); a full URL on the endpoint replaces the host (and scheme, if present).

6.2 Customizing the HTTP client

Retrofit delegates all networking to OkHttp. To add logging, authentication, timeouts, caching, or any header that belongs on every request, configure an OkHttpClient and pass it with .client(...):

OkHttpClient client = new OkHttpClient.Builder()
    .addInterceptor(new HttpLoggingInterceptor())
    .addInterceptor(chain -> {
      Request request = chain.request().newBuilder()
          .header("Authorization", "Bearer " + token)
          .build();
      return chain.proceed(request);
    })
    .build();

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl(API_URL)
    .client(client)
    .addConverterFactory(GsonConverterFactory.create())
    .build();

Note

If you don't call client(...) or callFactory(...), Retrofit creates a default OkHttpClient for you. For production apps you almost always want to supply your own so you control timeouts, interceptors, and the connection pool.


7. Changing the Return Type (Call Adapters)

By default a service method returns Call<T>. Call adapters let it return something else — an RxJava Observable, a CompletableFuture, a Guava ListenableFuture, or (with no extra dependency) the value of a Kotlin suspend function. Add them with addCallAdapterFactory(...).

7.1 Built-in call adapters

Return types Maven artifact
RxJava Observable, Single adapter-rxjava
RxJava 2 Observable, Flowable, Single, Completable, Maybe adapter-rxjava2
RxJava 3 Observable, Flowable, Single, Completable, Maybe adapter-rxjava3
Guava ListenableFuture adapter-guava
Java 8 CompletableFuture adapter-java8
Kotlin suspend functions none needed — built in
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl(API_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJava3CallAdapterFactory.create())
    .build();

public interface Service {
  @GET("users/{user}")
  Single<User> getUser(@Path("user") String user);
}

Note

On the JVM, the Java 8 CompletableFuture adapter is available without any extra dependency on Java 8+ runtimes. The adapter-java8 artifact exists for completeness; suspend and CompletableFuture support are built into the platform defaults.

7.2 Custom call adapters

To integrate a concurrency or task library Retrofit doesn't ship, extend CallAdapter.Factory and return an adapter that wraps the built-in Call. The Admin Guide documents the SPI in full.


8. Making Calls: Synchronous, Asynchronous, and Cancellation

A Call<T> represents a single request/response pair. Each Call can be used only once. To repeat a request (polling, retries), call clone() to get a fresh one.

8.1 Synchronous

Call<List<Contributor>> call = service.contributors("square", "retrofit");
Response<List<Contributor>> response = call.execute();   // blocks
if (response.isSuccessful()) {
  List<Contributor> body = response.body();
}

Warning

execute() blocks the calling thread and throws IOException on a network problem. Never call it on Android's main thread — do it on a background thread or use the asynchronous API.

8.2 Asynchronous

call.enqueue(new Callback<List<Contributor>>() {
  @Override public void onResponse(Call<List<Contributor>> call, Response<List<Contributor>> response) {
    if (response.isSuccessful()) {
      // response.body()
    } else {
      // response.errorBody() — server returned 4xx/5xx
    }
  }

  @Override public void onFailure(Call<List<Contributor>> call, Throwable t) {
    // network failure, or an error building the request / decoding the response
  }
});

Important

onResponse fires for any HTTP response, including 404 and 500. Always check response.isSuccessful() (true when the status is in [200, 300)) before reading body(). onFailure only fires for network errors or request/response processing errors — not for non-2xx statuses.

On Android, callbacks run on the main thread by default. On the plain JVM, callbacks run on the same background thread that performed the request.

8.3 Cancellation, state, and the request

call.cancel();              // attempt to cancel an in-flight or not-yet-started call
boolean canceled = call.isCanceled();
boolean executed = call.isExecuted();   // already executed or enqueued?
Request original = call.request();      // the OkHttp request this call will send
Call<T> retry = call.clone();           // a fresh, re-usable copy

8.4 Working with Response<T>

Method Returns
isSuccessful() true if the status code is in [200, 300)
code() HTTP status code
message() HTTP status message
headers() Response headers
body() Deserialized body of a successful response (may be null)
errorBody() Raw ResponseBody of an unsuccessful response
raw() The underlying okhttp3.Response

Tip

When the server returns an error status, the parsed body() is null and the unparsed error payload is in errorBody(). To map that error JSON onto your own error type, pull a converter from the Retrofit instance with retrofit.responseBodyConverter(...) and apply it to errorBody().


9. Kotlin: Coroutines and suspend Functions

Retrofit understands Kotlin suspend functions natively — no adapter dependency required. A suspend method creates the call, executes it asynchronously, and suspends the coroutine until it completes.

Return a Response<T> to inspect status and headers yourself:

@GET("users")
suspend fun getUser(): Response<User>

Or return the body type directly. On a non-2xx response, an HttpException is thrown:

@GET("users")
suspend fun getUser(): User
val service = retrofit.create(GitHubService::class.java)

try {
  val user: User = service.getUser()
  // use user
} catch (e: HttpException) {
  // e.code(), e.message(), e.response()
} catch (e: IOException) {
  // network failure
}

Important

A suspend function must not declare a return type of Call<T> — suspend functions already execute asynchronously, and Retrofit rejects this with a clear error telling you to return the body type directly.

Note

Use Unit as the body type (suspend fun ping(): Unit) when you don't care about the response payload — the body is discarded without deserialization. A @HEAD method must use Void (Java) or Unit (Kotlin) as its response type.


10. Testing Your API with retrofit-mock

The retrofit-mock module lets you stand in for the network so you can unit-test code that depends on your API interface, with realistic delays and a configurable failure rate.

implementation("com.squareup.retrofit2:retrofit-mock:3.0.0")
Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build();

// Wrap it in a MockRetrofit with simulated network behavior.
NetworkBehavior behavior = NetworkBehavior.create();
MockRetrofit mockRetrofit = new MockRetrofit.Builder(retrofit)
    .networkBehavior(behavior)
    .build();

BehaviorDelegate<GitHubService> delegate = mockRetrofit.create(GitHubService.class);

// Provide a hand-written mock implementation that returns canned responses.
GitHubService mockService = new MockGitHubService(delegate);

Inside your mock implementation, hand canned data back through the delegate:

public Call<List<Repo>> listRepos(String user) {
  List<Repo> repos = Arrays.asList(new Repo("octocat", "Hello-World"));
  return delegate.returningResponse(repos).listRepos(user);
}

NetworkBehavior lets you tune simulated latency, variance, and the probability of failures so tests exercise your error-handling paths. Calls (in the same module) provides helpers like Calls.response(...) and Calls.failure(...) to build standalone Call instances for tests.

Tip

For integration-style tests that exercise real serialization and the HTTP stack, point Retrofit at OkHttp's MockWebServer instead. retrofit-mock is for unit tests where you want to bypass the network entirely.


11. ProGuard / R8

If you use R8 (the default Android shrinker), Retrofit's shrinking and obfuscation rules are included automatically — there is nothing to do.

If you use ProGuard, add the rules from retrofit2.pro manually. You will likely also need rules for OkHttp and Okio, which are dependencies of Retrofit.

Caution

Aggressive shrinking can strip the generic type information Retrofit reads at runtime, causing failures like "Unable to create converter" or "Method return type must not include a type variable or wildcard" in release builds only. If static keep rules aren't enough for your interfaces, the retrofit-response-type-keeper annotation processor generates the extra rules for every type referenced by your service interfaces.


12. Troubleshooting

Error: API declarations must be interfaces.

Cause: You passed a class (or abstract class) to retrofit.create(...). Solution: Make your API declaration a Java/Kotlin interface. Prevention: Retrofit only generates implementations for interfaces.

Error: baseUrl must end in /: <url>

Cause: The base URL you supplied has no trailing slash. Solution: Add a trailing / — e.g. https://api.example.com/, not https://api.example.com. Prevention: Always end base URLs in /; see §6.1.

Error: Could not locate ResponseBody converter for <type>

Cause: No installed converter can deserialize the method's return type — most often you forgot to add a converter, or added it after a converter that consumes all types. Solution: Add the matching addConverterFactory(...) (e.g. GsonConverterFactory.create()). If using an Optional converter, install it before the real one. Prevention: Add a converter whenever a method returns anything other than ResponseBody.

Error: Could not locate call adapter for <type>

Cause: The method returns a type (such as Single or Observable) with no matching call adapter installed. Solution: Add the adapter factory — e.g. addCallAdapterFactory(RxJava3CallAdapterFactory.create()). Prevention: Each non-Call return type needs its adapter registered on the builder.

Error: IllegalStateException: Base URL required.

Cause: build() was called without baseUrl(...). Solution: Call .baseUrl(...) before .build(). It is the only required builder method.

Error: Suspend functions should not return Call

Cause: A Kotlin suspend method declared a Call<T> return type. Solution: Change the return type to the body type (e.g. User) or Response<User>. Prevention: suspend functions execute asynchronously already; never wrap them in Call.

Error: 'okhttp3.Response' is not a valid response body type. Did you mean ResponseBody?

Cause: A method used okhttp3.Response (the raw HTTP response) as its body type. Solution: Return Call<ResponseBody> for the raw body, or Call<okhttp3.Response> is not allowed — use Retrofit's Response<T> wrapper or ResponseBody.

Error: HEAD method must use Void or Unit as response type.

Cause: A @HEAD method declared a body type other than Void/Unit. Solution: Use Call<Void> (Java) or suspend fun … : Unit (Kotlin) — HEAD responses have no body.

Calls succeed but body() is null

Cause: The response was not successful (status outside [200, 300)), so the parsed body is null and the payload is in errorBody(). Solution: Check response.isSuccessful() first; read response.errorBody() for error responses.

@Path/@Body/@Part throws on null

Cause: @Path, @Body, and named @Part/@Field-map values may not be null. Solution: Use @Query/@Header (which silently omit null), or guarantee a non-null argument.

Works in debug, fails in release (Android)

Cause: R8/ProGuard stripped generic type information or kept rules your interfaces need. Solution: Ensure R8 is used (rules are automatic) or add retrofit2.pro for ProGuard; consider the retrofit-response-type-keeper processor. See §11.

@XmlJavaTypeAdapter ignored with the JAXB converter

Cause: A known open issue (#3349): JaxbConverterFactory does not recognize @XmlJavaTypeAdapter. Solution: Apply the type adapter at the field/package level your JAXB implementation honors, or pre/post-process the affected types until the issue is resolved.


13. FAQ

Q: Do I need to add OkHttp as a dependency? No. Retrofit pulls OkHttp in transitively. Declare it explicitly only to pin a particular OkHttp version.

Q: Should I create a new Retrofit instance per request? No. Create one Retrofit (and one OkHttpClient) and reuse them everywhere. They hold the connection pool and thread pools.

Q: How do I add an Authorization header to every request? Use an OkHttp interceptor on the OkHttpClient you pass to .client(...), not a per-method @Header. See §6.2.

Q: Can one method's URL be completely dynamic? Yes — omit the path on the verb annotation and annotate the first parameter with @Url. See §4.2.

Q: How do I send a DELETE with a body? @DELETE has no body. Use @HTTP(method = "DELETE", path = "...", hasBody = true) with a @Body parameter.

Q: How do I read the error body on a 4xx/5xx response? For Call, check response.isSuccessful() and read response.errorBody(). For suspend functions returning the body directly, catch HttpException and call e.response().

Q: What's the difference between @Query and @Field? @Query appends to the URL (?name=value). @Field builds a form-encoded request body and requires @FormUrlEncoded on the method.

Q: How do I upload a file? Use a @Multipart method with a @Part MultipartBody.Part parameter (build the part from an OkHttp RequestBody). See §4.6.

Q: Do suspend functions need the coroutines adapter? No. Coroutine support is built into Retrofit core — just declare a suspend fun.

Q: My non-ASCII header value is rejected. What now? Set allowUnsafeNonAsciiValues = true on @Header/@Headers/@HeaderMap. Note this isn't strictly HTTP-compliant.

Q: How do I test code that uses my API interface without hitting the network? Use retrofit-mock (MockRetrofit + BehaviorDelegate + NetworkBehavior) for unit tests, or OkHttp's MockWebServer for integration tests. See §10.

Q: Is Retrofit 3 a breaking change from 2? No. Retrofit 3.x maintains forward binary compatibility with 2.x. The main change in 3.0 is the upgrade to the Kotlin-based OkHttp 4.12.

Q: How do I retry or poll a request? A Call is single-use. Call call.clone() to get a fresh, executable copy for each attempt.

Q: Can I use multiple converters at once? Yes. Add them in priority order; Retrofit tries each until one handles the type. A common combo is scalars + a JSON converter.