The Issue
I'm running into an issue where I'm unable to access the 'Location' response header (or really, any response header other than 'Content-Length') from the response of an HTTP POST, using an HTTP Client inside of a Blazor Web App.
I've confirmed that the API we are calling is indeed returning the Location header; Captured via the developer tools within Chrome:

The Code in my Blazor Client Application
We don't do anything out of the ordinary when we create the Typed/HTTP Client and register it with DI; we're just setting up the timeout on the client and Polly Retries. The handlers we're registering are for Authentication and logging respectively.
public static IServiceCollection AddInternalApiClient(this IServiceCollection services)
{
_ = services ?? throw new ArgumentNullException(nameof(services));
services.AddOptions<InternalApiClientOptions>().Configure<IConfiguration>((options, configuration) => configuration.GetSection(nameof(InternalApiClientOptions)).Bind(options));
services
.AddHttpClient<InternalApiClient>((serviceProvider, client) =>
{
var config = serviceProvider.GetRequiredService<IOptions<InternalApiClientOptions>>().Value;
client.BaseAddress = new Uri(config.BaseAddress, UriKind.Absolute);
client.Timeout = config.Timeout;
})
.AddHttpMessageHandler<ApiAuthorizationMessageHandler>()
.AddHttpMessageHandler<ApiMetricsHandler>()
.AddPolicyHandler((serviceProvider, _) =>
{
var config = serviceProvider.GetRequiredService<IOptions<InternalApiClientOptions>>().Value;
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(config.Retries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(config.Backoff, retryAttempt)));
});
return services;
}
public class ApiAuthorizationMessageHandler : AuthorizationMessageHandler
{
public ApiAuthorizationMessageHandler(
IAccessTokenProvider provider,
NavigationManager navigationManager,
string[] authorizedUrlArray,
string scope)
: base(provider, navigationManager)
{
ConfigureHandler(
authorizedUrls: authorizedUrlArray,
scopes: new[] { scope });
}
}
public class ApiMetricsHandler : DelegatingHandler
{
private readonly SessionManager _sessionManager;
private readonly AppVersionInfo _appVersionInfo;
private readonly IMetricsMonitor _metricsMonitor;
public ApiMetricsHandler(SessionManager sessionManager, AppVersionInfo appVersionInfo, IMetricsMonitor metricsMonitor)
{
_sessionManager = sessionManager;
_appVersionInfo = appVersionInfo;
_metricsMonitor = metricsMonitor;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Dictionary<string, object> transactionDetails = new Dictionary<string, object>()
{
{ "SessionId", _sessionManager.SessionId },
// Etc.
};
_metricsMonitor.TrackEvent(_sessionManager.CurrentTransactionId, transactionDetails);
return await base.SendAsync(request, cancellationToken);
}
}
As for the Typed Client itself, its a standard NSwag-generated client. Below is a HEAVILY edited version for brevity.
public partial class InternalApiClient
{
private HttpClient _httpClient;
private Lazy<JsonSerializerSettings> _settings;
public InternalApiClient(HttpClient httpClient)
{
_httpClient = httpClient;
_settings = new Lazy<JsonSerializerSettings>(() =>
{
var settings = new JsonSerializerSettings();
UpdateJsonSerializerSettings(settings);
return settings;
});
}
protected JsonSerializerSettings JsonSerializerSettings { get { return _settings.Value; } }
// FWIW, these aren't defined elsewhere.
partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings);
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, string url);
partial void PrepareRequest(HttpClient client, HttpRequestMessage request, StringBuilder urlBuilder);
partial void ProcessResponse(HttpClient client, HttpResponseMessage response);
public async Task<SwaggerResponse> PostAsync(Request body, CancellationToken cancellationToken)
{
var urlBuilder_ = new StringBuilder();
urlBuilder_.Append("endpoint/path");
var client_ = _httpClient;
try
{
using (var request_ = new HttpRequestMessage())
{
var content_ = new StringContent(JsonConvert.SerializeObject(body, _settings.Value));
content_.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json");
request_.Content = content_;
request_.Method = new HttpMethod("POST");
PrepareRequest(client_, request_, urlBuilder_);
var url_ = urlBuilder_.ToString();
request_.RequestUri = new System.Uri(url_, UriKind.RelativeOrAbsolute);
PrepareRequest(client_, request_, url_);
var response_ = await client_.SendAsync(request_, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false);
try
{
var headers_ = Enumerable.ToDictionary(response_.Headers, h_ => h_.Key, h_ => h_.Value);
if (response_.Content != null && response_.Content.Headers != null)
{
foreach (var item_ in response_.Content.Headers)
headers_[item_.Key] = item_.Value;
}
ProcessResponse(client_, response_);
var status_ = ((int)response_.StatusCode).ToString();
if (status_ == "400")
{
var objectResponse_ = await ReadObjectResponseAsync<ProblemDetails>(response_, headers_).ConfigureAwait(false);
throw new ApiException<ProblemDetails>("bad request", (int)response_.StatusCode, objectResponse_.Text, headers_, objectResponse_.Object, null);
}
else
if (status_ == "202")
{
return new SwaggerResponse((int)response_.StatusCode, headers_);
}
// Etc
else
if (status_ != "200" && status_ != "204")
{
var responseData_ = response_.Content == null ? null : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("The HTTP status code of the response was not expected (" + (int)response_.StatusCode + ").", (int)response_.StatusCode, responseData_, headers_, null);
}
return new SwaggerResponse((int)response_.StatusCode, headers_);
}
finally
{
if (response_ != null)
response_.Dispose();
}
}
}
finally
{
}
}
}
Does this work elsewhere?
I ran a test using a different project that uses the same generated API Client Class, in particular an Azure Functions project. When running in that project, I was able to receive all 8 response headers in the image above.
The DI setup logic is slightly different, but the HTTP Client setup poritions remain the same:
public static IServiceCollection AddInternalApiClient(this IServiceCollection services)
{
_ = services ?? throw new ArgumentNullException(nameof(services));
services.AddOptions<InternalApiClientOptions>().Configure<IConfiguration>((options, configuration) => configuration.GetSection(nameof(InternalApiClientOptions)).Bind(options));
services
.AddHttpClient<InternalApiClient>((serviceProvider, client) =>
{
var config = serviceProvider.GetRequiredService<IOptions<InternalApiClientOptions>>().Value;
client.BaseAddress = new Uri(config.BaseAddress, UriKind.Absolute);
client.Timeout = config.Timeout;
})
.AddAzureIdentityCredentials(scopeFactory: (sp, _) =>
{
var config = sp.GetRequiredService<IOptions<InternalApiClientOptions>>().Value;
return $"{config.Scope}/.default";
})
.AddPolicyHandler((serviceProvider, _) =>
{
var config = serviceProvider.GetRequiredService<IOptions<InternalApiClientOptions>>().Value;
return HttpPolicyExtensions
.HandleTransientHttpError()
.WaitAndRetryAsync(config.Retries, retryAttempt => TimeSpan.FromSeconds(Math.Pow(config.Backoff, retryAttempt)));
});
return services;
}
Final Thoughts
Because I'm able to access the response headers when using the same API Client in a different project, it feels like the issue is either Blazor as a whole, or my usage of the API client through it (via the Message Handlers). I've seen other questions/bugs regarding the Blazor and headers:
- How to Access Httpclient headers in Blazor WASM Client
- HttpClient headers are empty in blazor wasm
- Blazor WASM C# .NET 6 Content-Disposition is not Accessible
The responses for these all seem to be: Update the CORS policies on the Server. I'm not sure if this IS a CORS issue though, as a different project can receive all of the headers just fine (and the 'Content-Length' header is always received no matter what). The Server itself is used by other Apps than the two I mentioned, so I'm hesitant to start adding the 'AddCors/UseCors' calls that the linked issues mentioned.
Any insight, references would be greatly appreciated.