Repository navigation
Description
Description
Depending on the order in which in which the Credentials and UseDefaultCredentials are initialized for HttpClientHandler, there's a difference in behavior between .NET Framework and Core. More specifically, .NET Framework does not care about order of initialization of those properties, whereas in .NET Core, the order might lead to an unexpected outcome.
Reproduction Steps
We're using the following method to initialize an HttpClient that can be used to access a Kerberized web service. In the common case, the calling user's credentials should be used to authenticate to the service, although we provide the option to specify credentials explicitly as well for certain scenarios. This (library) method may get called by applications in either .NET Framework or Core.
The two important lines in the example below are named A and B for ease of explanation subsequently.
public static HttpClient CreateHttpClient(Uri uri, ICredentials credentials = null) =>
new HttpClient(new HttpClientHandler
{
UseDefaultCredentials = credentials == null, // line A
Credentials = credentials, // line B
PreAuthenticate = true
})
{
BaseAddress = uri
};Expected behavior
In both .NET Framework and Core, the HttpClient should facilitate access to the service (identified by uri) as follows:
- If
credentialsis explicitly provided, it should use that to access the service, and - If
credentialsis not provided/null, the service should be accessed using the calling user's credentials ("default credentials").
Actual behavior
In .NET Framework, the HttpClient works as desired in either case, largely because the properties in question are backed by independent fields, applied in SetDefaultOptions(). Also, in .NET Core, if credentials is explicitly provided, the HttpClient uses that correctly.
However, if credentials is left null in .NET Core, the current user's credentials are not used. This is due to the fact that in .NET Core, both UseDefaultCredentials and Credentials properties operate on the same _underlyingHandler.Credentials property. So, with the the function as written in the repro and credentials = null, the following happens:
- In Line A, the right-hand expression evaluates to true, causing
_underlyingHandler.Credentials = CredentialCache.DefaultCredentialswithin the setter. - Line B then comes along and (un)sets
_underlyingHandler.Credentials = null. - At this point, the effect of
UseDefaultCredentialsfrom line A is lost, so when making the API call, we get a HTTP 401.
"Luckily", if Line A and B were inverted, things work out correctly in .NET Core regardless of credentials (and .NET Framework continues to not care about the order):
var client = new HttpClient(new HttpClientHandler
{
Credentials = credentials, // line B
UseDefaultCredentials = credentials == null, // line A
...- If
credentialsis null:- Line B sets
_underlyingHandler.Credentials = null, and - Line A then sets
_underlyingHandler.Credentials = CredentialCache.DefaultCredentials.
- Line B sets
- If
credentialsis non-null valueX:- Line B sets
_underlyingHandler.Credentials = Xand - In line A, the RHS evaluates to false, and the inner condition isn't met, so the setter effectively no-op's
- Line B sets
So in either scenario, we now have the desired behavior, and a function that works across Core and Framework.
Regression?
Yes: as explained, the code sample provided in the repro works as intended in .NET Framework, but the order of properties introduces a regression in .NET Core.
Known Workarounds
Inverting the order in which the properties in question are specified (i.e., ensure that Credentials is initialized before UseDefaultCredentials) is one way of working around the issue in a way that the sample works on .NET Core (and Framework).
Configuration
Beyond Core vs. Framework, I don't think the specifics really matter, but for completeness: Windows 10 22H2 (March CU, 19045.2728), .NET Core 6.0.15, .NET Framework 4.8, 64-bit.
Other information
This issue looks fairly similar to the one flagged for SmtpClient and addressed in #41271, although I couldn't find a corresponding issue/PR for HttpClient. I suspect that (similar to that one,) it should be possible to make the behavior consistent/intuitive here by having backing fields to better track the intent; happy to take a crack at the PR if we can arrive at a consensus for the direction we'd like to take.
Activity
antonfirsov commentedon Apr 12, 2023
While this might be perceived as a breaking change (or regression) when migrating from .NET Framework, I think we should keep the current behavior, and document the difference as intentional.
The only way
HttpClientHandlercan pass the credentials to the underlying handler is to set_underlyingHandler.Credentials, which means there is no clean way to maintainUseDefaultCredentialsandCredentialsas independent properties. IMOHttpClientHandler.Credentialsshould always return the credentials that are being passed to the underlying handler, meaning thatCredentialschanging after a change toUseDefaultCredentialsis an expected "side effect" for me.TBH I don't see the why do we have 2 separate properties to control the same thing. Having an extra
UseDefaultCredentialsproperty seems like a bad design choice copied from legacyHttpWebRequest,SmtpClientetc. API-s.@kedia990 note that you could simplify your code as following to make the intention clear:
@dotnet/ncl any objections against turning this into a documentation issue?
antonfirsov commentedon Apr 13, 2023
Triage: we could special-case
UseDefaultCredentials == trueinCredentials's setter to avoid clearing the credentials in that case, but with that we may break other users depending on the .NET Core/6.0+ behavior. Since we have only one report, it seems to be better to keep & document the behavior for now. We can always reevaluate if more customers hit this issue.kedia990 commentedon Apr 16, 2023
Thanks for your responses, @antonfirsov.
Yeah, that was kinda what I had in mind, although if we think that that itself carries the risk of breaking callers that now rely on .NET Core behavior, we could just document this as a difference / migration caveat between .NET Framework -> Core.
I don't know why I assumed that
CredentialCache.DefaultCredentialswouldn't be available in .NET Standard 2.0. It clearly is, and that's indeed an improvement code-clarity-wise. Thanks for the tip.