Skip to content

Response details and success checks

Run the complete page example.

Sometimes the reply's data is only part of what your app needs. You may also need its status, a header from the service, or details about why the call failed. ApiResponse<T> keeps those pieces together with the value Refit read from the body.

The wrapper helps you tell a successful call from a failed connection or an unreadable reply. This page shows which checks to make before using the value and how to release the response afterward.

Check a reply

1. Declare a wrapped return type. Use Task<ApiResponse<Person>> or Task<IApiResponse<Person>> on your interface method. Person is the model from your first request.

2. Configure generated JSON metadata. Reuse the JSON context and serializer setup. Register Person, the body type. Do not register ApiResponse<Person> as a JSON root. Refit creates that wrapper locally; the server sends only the body's JSON.

3. Guard the result and dispose the wrapper. The return-type example shows the normal API call. The example below constructs the same kind of wrapper directly. This is useful when writing a client adapter or a test.

using HttpRequestMessage request = new(HttpMethod.Get, "https://people.example/person");
using HttpResponseMessage message = new(HttpStatusCode.OK) { RequestMessage = request };
using ApiResponse<Person> response = new(message, new(1, "Ada"), settings);
using ApiResponse<Person> explicitError = new(message, new(1, "Ada"), settings, error: null);
Console.WriteLine(response.IsReceived); // True
Console.WriteLine(response.StatusCode); // OK
Console.WriteLine(response.RequestMessage.RequestUri);
Console.WriteLine(response.Settings == settings); // True
if (response.IsSuccessfulWithContent)
{
    Console.WriteLine(response.Content.Name); // Ada
}

Both response-only constructors require a non-null HttpResponseMessage with RequestMessage set. A missing response raises ArgumentNullException. A missing associated request raises ArgumentException. The overload with error records the supplied ApiExceptionBase; it does not infer an error from the status. The content argument is your deserialized value. The constructor does not parse JSON.

The five-argument constructor takes the request separately. It accepts a null response for a transport failure. A transport failure happens before Refit receives an HTTP response. The transport example below shows that constructor.

Status success and content success differ

A status from 200 through 299 is an HTTP success. It does not prove that the body matches your C# model. The response can have status 200 and contain invalid JSON. The runnable example supplies a 200 reply with the text not JSON:

using ApiResponse<Person> malformed = await api.GetMalformedAsync(CancellationToken.None);
Console.WriteLine(malformed.IsReceived); // True
Console.WriteLine(malformed.IsSuccessStatusCode); // True
Console.WriteLine(malformed.IsSuccessful); // False
Console.WriteLine(malformed.HasContent); // False
_ = await malformed.EnsureSuccessStatusCodeAsync();
if (malformed.HasResponseError(out ApiException? readError))
{
    Console.WriteLine(readError.InnerException?.GetType().Name); // JsonException
}

EnsureSuccessStatusCodeAsync checks only the status. EnsureSuccessfulAsync also checks Error. Each returns the same response on success. Await each returned ValueTask once. Use the full-success check for model replies. Use the status-only check when your code handles body errors itself.

PropertyMeaning
IsReceivedA response message exists. False means no response arrived.
IsSuccessStatusCodeA response exists and its status is 200–299.
IsSuccessfulThe status succeeds and Error is null.
HasContentContent is non-null. For a value type, its default value is also non-null.
IsSuccessfulWithContentBoth IsSuccessful and HasContent are true.
ContentThe deserialized reply value, or default when unavailable.
ErrorA captured ApiExceptionBase, or null. An unsuccessful manually constructed wrapper may have no error.
SettingsThe settings supplied to the concrete wrapper. It keeps the same instance.
RequestMessageThe request associated with this reply. The interface permits null; the concrete wrapper returns its constructor argument.
StatusCode, ReasonPhrase, VersionThe response status, reason text and HTTP version. Null when no response exists.
HeadersThe response headers. Null when no response exists.
ContentHeadersThe body headers, such as its media type. Null when unavailable.

Success alone does not promise a body. A 204 reply, for example, can succeed without content. The flags do not validate an app rule such as “the name must not be empty”. Apply that rule yourself.

Guards through either interface

IApiResponse carries status, headers and errors. IApiResponse<out T> adds typed content and its presence flags. The out T lets a wrapper for a derived model be read through an interface for its base model. ApiResponseExtensions supplies both guards for both interfaces:

IApiResponse<Person> typed = response;
IApiResponse untyped = response;
_ = await response.EnsureSuccessStatusCodeAsync();
_ = await response.EnsureSuccessfulAsync();
_ = await typed.EnsureSuccessStatusCodeAsync();
_ = await typed.EnsureSuccessfulAsync();
_ = await untyped.EnsureSuccessStatusCodeAsync();
_ = await untyped.EnsureSuccessfulAsync();

The concrete guards return ValueTask<ApiResponse<T>>. The generic interface guards return ValueTask<IApiResponse<T>>. The non-generic interface guards return ValueTask<IApiResponse>. Interface guards reject a null receiver with ArgumentNullException.

On failure, an interface guard throws the captured error. If no error was recorded, it throws InvalidOperationException. It does not create an HTTP error or dispose the wrapper. Keep the using declaration around the call. The concrete guard creates an ApiException when a received unsuccessful reply has no captured error. It disposes the response before throwing that error.

When no reply arrives

HasRequestError(out ApiRequestException?) checks for a failure before a response arrives. HasResponseError(out ApiException?) checks for a received-response or body-reading error. Both return false and assign null when that error kind is absent. ValidationApiException also counts as a response error.

The three ApiRequestException constructors let you supply a cause, a message, or both. The cause-only overload uses the cause's message and rejects a null cause. All retain the request, method and settings you supply.

using HttpRequestMessage request = new(HttpMethod.Get, "https://people.example/person");
HttpRequestException cause = new("Connection unavailable.");
ApiRequestException fromCause = new(request, request.Method, settings, cause);
ApiRequestException withMessage = new("Could not contact people API.", request, request.Method, settings);
ApiRequestException withBoth = new("Could not contact people API.", request, request.Method, settings, cause);
using ApiResponse<Person> missing = new(request, response: null, content: null, settings, fromCause);
Console.WriteLine(missing.IsReceived); // False
Console.WriteLine(missing.StatusCode is null); // True
if (missing.HasRequestError(out ApiRequestException? sendError))
{
    Console.WriteLine(sendError.Message); // Connection unavailable.
}

Implementation discrepancy: the concrete guards check for a response before using the captured error. With a null response, both concrete guards throw InvalidOperationException rather than the documented ApiRequestException. The interface guards surface the captured transport error. The example checks this difference:

IApiResponse<Person> typed = missing;
try
{
    _ = await typed.EnsureSuccessfulAsync();
    throw new InvalidOperationException("The interface guard should throw.");
}
catch (ApiRequestException error)
{
    SampleCheck.Equal(fromCause, error);
}

try
{
    _ = await missing.EnsureSuccessfulAsync();
    throw new InvalidOperationException("The concrete guard should throw.");
}
catch (InvalidOperationException error)
{
    SampleCheck.Equal("The response is unavailable for this API response.", error.Message);
}

Use HasRequestError or an interface guard when handling this case. Read error bodies and problem details for the exception properties. The response examples contain the executable reproduction.

Ownership

ApiResponse<T>.Dispose() disposes the underlying HTTP response once. Repeated calls are safe. It does not dispose the request separately. Read any needed body or headers before disposing the wrapper. Disposal does not change its status flags or erase the stored value.

The ownership example creates separate request and response streams. These checks confirm that disposing the wrapper closes only the response stream:

response.Dispose();
response.Dispose();
SampleCheck.Equal(false, responseStream.CanRead);
SampleCheck.Equal(true, requestStream.CanRead);

Response API reference

MemberDescriptionParametersReturns or value
ApiRequestExceptionRepresents a failure while Refit sends a request before a response arrives.None.An ApiExceptionBase that retains request context and may wrap the sending exception.
ApiResponse<T>Wraps a deserialized body, HTTP metadata, settings, and a captured error.T: the body type.A sealed IApiResponse<T> that disposes the received response.
IApiResponseDefines status, metadata, error, and disposal members for a Refit response.None.The base response contract.
IApiResponse<T>Adds a covariant deserialized body and body-presence checks to IApiResponse.out T: the body type read by callers.The typed response contract.
ApiResponseExtensionsProvides success guards for generic and non-generic response interfaces.None.A static extension class.
ApiResponse<T>(HttpRequestMessage request, HttpResponseMessage? response, T? content, RefitSettings settings, ApiExceptionBase? error = null)Creates a response wrapper that keeps the request, optional HTTP response, deserialized content, settings, and captured error together.HttpRequestMessage request; HttpResponseMessage? response; T? content; RefitSettings settings; ApiExceptionBase? error = null.New response wrapper. response may be null for a transport failure; error defaults to null.
ApiResponse<T>(HttpResponseMessage response, T? content, RefitSettings settings)Creates a wrapper for a received response with no captured error.HttpResponseMessage response; T? content; RefitSettings settings.A new wrapper. response and response.RequestMessage must be non-null.
ApiResponse<T>(HttpResponseMessage response, T? content, RefitSettings settings, ApiExceptionBase? error)Creates a wrapper for a received response and a supplied captured error.HttpResponseMessage response; T? content; RefitSettings settings; ApiExceptionBase? error.A new wrapper. response and response.RequestMessage must be non-null.
ApiResponse<T>.Dispose()Disposes the received HTTP response once.None.void; it does not dispose RequestMessage.
ApiResponse<T>.EnsureSuccessStatusCodeAsync()Guards only the HTTP status.None.ValueTask<ApiResponse<T>>; returns this for 2xx. Otherwise it disposes and throws Error or a created ApiException; with no response it throws InvalidOperationException.
ApiResponse<T>.EnsureSuccessfulAsync()Guards the HTTP status and captured error.None.ValueTask<ApiResponse<T>>; returns this when IsSuccessful is true. Failure behavior matches the status guard.
ApiResponse<T>.HasRequestError(out ApiRequestException? error)Checks for a captured transport error and returns it through the out parameter.out ApiRequestException? error.bool; true when Error is a request error.
ApiResponse<T>.HasResponseError(out ApiException? error)Checks for a captured response error and returns it through the out parameter.out ApiException? error.bool; true when Error is a response error.
ApiResponse<T>.ContentStores the deserialized response body, or the default value when no body was read.None.T?: the stored response body, or default(T) when no value was read. For a non-nullable value type, this can be a value such as 0.
ApiResponse<T>.ContentHeadersExposes headers belonging to the received response body.None.HttpContentHeaders?: content headers, or null when no response content exists.
ApiResponse<T>.ErrorStores the captured transport, HTTP-status, or deserialization error.None.ApiExceptionBase?: the captured transport, HTTP, or deserialization error.
ApiResponse<T>.HasContentReports whether the deserialized Content is non-null.None.bool; Content is non-null.
ApiResponse<T>.IsSuccessfulWithContentReports whether the response succeeded without an error and has non-null Content.None.bool: true when the status is successful, no error was captured, and Content is non-null.
ApiResponse<T>.HeadersExposes headers from the received HTTP response.None.HttpResponseHeaders?: the received response headers, or null when no response arrived.
ApiResponse<T>.IsReceivedReports whether an HTTP response message was received.None.bool: true when an HTTP response arrived.
ApiResponse<T>.IsSuccessStatusCodeReports whether the received status code is in the 2xx range.None.bool: true when a received response has a 2xx status.
ApiResponse<T>.IsSuccessfulReports whether the status is 2xx and no error was captured; it does not require content.None.bool: true when the status is 2xx and no error was captured. It does not promise body content.
ApiResponse<T>.ReasonPhraseExposes the reason phrase returned with the HTTP status.None.string?: the server reason phrase, or null when none is available.
ApiResponse<T>.RequestMessageExposes the request associated with the response wrapper.None.HttpRequestMessage: the request associated with this wrapper.
ApiResponse<T>.SettingsExposes the RefitSettings used to process the response.None.RefitSettings: the settings instance supplied to the constructor.
ApiResponse<T>.StatusCodeExposes the received HTTP status code, or null when no response arrived.None.HttpStatusCode?: the received status, or null when no response arrived.
ApiResponse<T>.VersionExposes the HTTP version used by the received response.None.Version?: the received HTTP version, or null when no response arrived.
IApiResponse.HasRequestError(out ApiRequestException? error)Checks for a captured transport error and returns it through the out parameter.out ApiRequestException? error.bool: true and assigns the transport error when the request failed before a response; otherwise false and null.
IApiResponse.HasResponseError(out ApiException? error)Checks for a captured response error and returns it through the out parameter.out ApiException? error.bool: true and assigns the response or body-reading error; otherwise false and null.
IApiResponse.HeadersExposes headers from the received HTTP response.None.HttpResponseHeaders?: the received response headers, or null when no response arrived.
IApiResponse.ContentHeadersExposes headers belonging to the received response body.None.HttpContentHeaders?: headers for the received response body, or null when they are unavailable.
IApiResponse.IsReceivedReports whether an HTTP response message was received.None.bool: true when an HTTP response arrived.
IApiResponse.IsSuccessStatusCodeReports whether the received status code is in the 2xx range.None.bool: true when a received response has a 2xx status.
IApiResponse.IsSuccessfulReports whether the status is 2xx and no error was captured; it does not require content.None.bool: true when the status is 2xx and no error was captured. It does not promise body content.
IApiResponse.StatusCodeExposes the received HTTP status code, or null when no response arrived.None.HttpStatusCode?: the received status, or null when no response arrived.
IApiResponse.ReasonPhraseExposes the reason phrase returned with the HTTP status.None.string?: the server reason phrase, or null when none is available.
IApiResponse.RequestMessageExposes the request associated with the response wrapper.None.HttpRequestMessage?: the request that led to the response, or null when unavailable.
IApiResponse.VersionExposes the HTTP version used by the received response.None.Version?: the received HTTP version, or null when no response arrived.
IApiResponse.ErrorStores the captured transport, HTTP-status, or deserialization error.None.ApiExceptionBase?: a captured transport, HTTP, or deserialization error. An unsuccessful response can have no captured error.
IApiResponse<T>.ContentStores the deserialized response body, or the default value when no body was read.None.T?: the stored response body, or default(T) when no value was read. For a non-nullable value type, this can be a value such as 0.
IApiResponse<T>.HasContentReports whether the deserialized Content is non-null.None.bool: true when Content is non-null.
IApiResponse<T>.IsSuccessfulWithContentReports whether the response succeeded without an error and has non-null Content.None.bool: true when IsSuccessful is true and Content is non-null.
ApiRequestException(HttpRequestMessage message, HttpMethod httpMethod, RefitSettings refitSettings, Exception innerException)Creates a transport exception with the request, HTTP method, settings, and supplied message or inner cause.HttpRequestMessage message; HttpMethod httpMethod; RefitSettings refitSettings; Exception innerException.New transport exception using the non-null cause's message.
ApiRequestException(string exceptionMessage, HttpRequestMessage message, HttpMethod httpMethod, RefitSettings refitSettings)Creates a transport exception with the request, HTTP method, settings, and supplied message or inner cause.string exceptionMessage; HttpRequestMessage message; HttpMethod httpMethod; RefitSettings refitSettings.New transport exception with the supplied message.
ApiRequestException(string exceptionMessage, HttpRequestMessage message, HttpMethod httpMethod, RefitSettings refitSettings, Exception? innerException)Creates a transport exception with the request, HTTP method, settings, and supplied message or inner cause.string exceptionMessage; HttpRequestMessage message; HttpMethod httpMethod; RefitSettings refitSettings; Exception? innerException.New transport exception with the supplied message and optional cause.
ApiResponseExtensions.EnsureSuccessStatusCodeAsync<T>(IApiResponse<T> response)Guards only the HTTP status of a typed response.IApiResponse<T> response.ValueTask<IApiResponse<T>>; returns the same response for 2xx. It rejects null, and otherwise throws Error or InvalidOperationException without disposing.
ApiResponseExtensions.EnsureSuccessfulAsync<T>(IApiResponse<T> response)Guards the HTTP status and captured error of a typed response.IApiResponse<T> response.ValueTask<IApiResponse<T>>; returns the same response when IsSuccessful is true. Failure behavior matches the status guard.
ApiResponseExtensions.EnsureSuccessStatusCodeAsync(IApiResponse response)Guards only the HTTP status of a non-generic response.IApiResponse response.ValueTask<IApiResponse>; returns the same response for 2xx. It rejects null, and otherwise throws Error or InvalidOperationException without disposing.
ApiResponseExtensions.EnsureSuccessfulAsync(IApiResponse response)Guards the HTTP status and captured error of a non-generic response.IApiResponse response.ValueTask<IApiResponse>; returns the same response when IsSuccessful is true. Failure behavior matches the status guard.

Types: HttpRequestMessage, HttpResponseMessage, HttpMethod, HttpStatusCode, Version, and ValueTask.

Production source: ApiResponse{T}.cs, IApiResponse.cs, IApiResponse{T}.cs, ApiResponseExtensions.cs, and ApiRequestException.cs.