为什么 HttpClient 的 BaseAddress 不起作用?

527

考虑以下代码,其中BaseAddress定义了一个部分URI路径。

using (var handler = new HttpClientHandler())
using (var client = new HttpClient(handler))
{
    client.BaseAddress = new Uri("http://something.com/api");
    var response = await client.GetAsync("/resource/7");
}

我希望它执行一个GET请求,访问http://something.com/api/resource/7。但它没有。

搜索一番后,我找到了这个问题和答案:HttpClient with BaseAddress。建议在BaseAddress的末尾放置/

using (var handler = new HttpClientHandler())
using (var client = new HttpClient(handler))
{
    client.BaseAddress = new Uri("http://something.com/api/");
    var response = await client.GetAsync("/resource/7");
}

它仍然不起作用。这是文档:HttpClient.BaseAddress 这里发生了什么?


1
可能是带有BaseAddress的HttpClient的重复问题。 - George Lanetz
3
反向重复的问题已经被提出。我写这个问题是因为另一个问题的表述方式对于遇到同样问题的人来说并不太容易发现,我在这里回答是因为那个问题的回答漏掉了一个重要的点。 - Timothy Shields
1
抱歉,这个问题稍后再回答。 - George Lanetz
4
通常来说,最具“规范性”的问题会成为其他重复问题所指向的问题。那个问题是关于一个用户遇到的单个问题,而不像一个常见问题解答。所以它不能起到这样的作用。 - Timothy Shields
4
请注意,我在这个问题中引用了另一个问题,并解释了为什么那个问题和答案无法解决这个问题。 - Timothy Shields
5个回答

1276

事实证明,在将前导或尾随正斜杠包括或排除在传递给GetAsync方法(或其他任何HttpClient方法)的BaseAddress和相对URI上的四种可能排列中,只有一种排列有效。您必须在BaseAddress末尾放置一个斜杠,并且不得在相对URI开头放置斜杠,如下例所示。

using (var handler = new HttpClientHandler())
using (var client = new HttpClient(handler))
{
    client.BaseAddress = new Uri("http://something.com/api/");
    var response = await client.GetAsync("resource/7");
}

虽然我自己回答了我的问题,但考虑到这种不友好的行为未被记录,我想在此贡献解决方案。我的同事和我花了大部分时间来修复一个问题,最终发现是由于 HttpClient 的这个怪异特性导致的。


10
谢谢。这解决了我在两天内一直苦苦挣扎的问题,因为我一直在Azure和IIS之间切换,又切回IIS Express,而IIS Express会非常无礼地忽略错误放置或额外的正斜杠。一旦在我的“RestClient”的基类中设置好后,几乎是看不见的,也没有引起任何注意,我在断点等地方从未看到完整的URL。 - ProfK
79
我可以确认,在.NET Core中,这种异常情况(以及解决方法)仍然存在。感谢让我少掉头发的 Timothy。 - Nate Barbettini
13
这是因为在构建请求时,如果没有斜杠作为结尾,它会省略最后一部分。因此,它会访问http://something.com/resource/7。如果将基础地址设置为http://something/com(无论是否带有斜杠),在api/resource/7的开头放置斜杠也不重要。如果没有斜杠作为结尾,基础地址的最后一部分会被视为文件并在构建请求时被省略。 - Piotr Perak
28
糟糕的实现,他们为什么不修复它? - timmkrause
23
哇塞!谢谢你!8年后,使用Visual Studio 2022和.NET 6.0,我们仍然要处理这种愚蠢的设计! - Jonathan Wood
显示剩余7条评论

129
参考解析由RFC 3986统一资源标识符(URI):通用语法描述。这正是它应该工作的方式。为了保留基本URI路径,您需要在基本URI的末尾添加斜杠,并删除相对URI的开头的斜杠。
如果基本URI包含非空路径,则合并过程会丢弃其最后一部分(最后一个/之后)。相关部分
5.2.3. 合并路径
上述伪代码是指合并相对路径引用与基本URI路径的“合并”例程。具体步骤如下:
- 如果基本URI具有已定义的授权组件和空路径,则返回由“/”与引用路径连接而成的字符串; - 否则,返回由引用路径组件附加到基本URI路径的最后一个段之前的所有段(即,排除基本URI路径中最右边的“/”后面的任何字符,或者如果基本URI路径不包含任何“/”字符,则排除整个基本URI路径)。
如果相对URI以斜杠开头,则称为绝对路径相对URI。在这种情况下,合并过程将忽略所有基本URI路径。有关更多信息,请参阅5.2.2. 转换引用部分。

50
好的,但是像 HttpClient 这样的客户端库应该为我们屏蔽这些奇特的实现细节。 - Jamie Ide
17
回答涵盖为什么事情会以这种方式运作的原因,并附上相关链接和引用,这样非常好。即使这并没有直接帮助解决问题,它也能提供很大帮助。 - Rast
1
这就是为什么你有1万个星星,而我只有174个银色的原因。 - knile
@JamieIde 它必须保护你免受此类问题的唯一方法是在尝试配置此类格式错误的基本 URL 时抛出错误。 - Dragas

18

如果你正在使用httpClient.SendAsync(),则没有像Get或其他动词特定方法的重载那样提供相对Uri的字符串重载。

但是,您可以通过将UriKind.Relative作为第二个参数来创建相对Uri。

var httpRequestMessage = new HttpRequestMessage
{
    Method = httpMethod,
    RequestUri = new Uri(relativeRequestUri, UriKind.Relative),
    Content = content
};

using var httpClient = HttpClientFactory.CreateClient("XClient");
var response = await httpClient.SendAsync(httpRequestMessage);
var responseText = await response.Content.ReadAsStringAsync();

2
只有当您的基地址仅包含主机地址和域名时,此方法才有效。例如,如果您将HttpClientBaseAddress属性设置为new Uri("https://mywebservice.com/api/public")并创建一个HttpRequestMessage,将RequestUri = new Uri("users/123456")传递给构造函数,则在生成的请求中使用的URL将是https://mywebservice.com/users/123456。这让我很烦恼,因为我的所有请求都使用公共URL前缀(例如https://mywebservice.com/api/public),所以将其设置为基地址会很好。 - Philip Stratford

4

我也遇到了与 BaseAddress 相关的问题。我决定不使用 BaseAddress,最简单的解决方法是添加一个简单的一行代码:

Uri GetUri(string path) => new Uri("http://something.com/api" + path);

那么你的代码将变成:

Uri GetUri(string path) => new Uri("http://something.com/api" + path);
using (var handler = new HttpClientHandler())
using (var client = new HttpClient(handler))
{
    // Remove BaseAddress completely
    // client.BaseAddress = new Uri("http://something.com/api");
    var response = await client.GetAsync(GetUri("/resource/7"));
}

我没有调查使用BaseAddress的利弊,但对我而言,这个方法非常完美。希望这能帮到某些人。


0
遇到了HTTPClient的问题,即使有建议,仍然无法进行身份验证。结果发现我需要在相对路径中添加一个尾部'/'。
例如:
var result = await _client.GetStringAsync(_awxUrl + "api/v2/inventories/?name=" + inventoryName);

var result = await _client.PostAsJsonAsync(_awxUrl + "api/v2/job_templates/" + templateId+"/launch/" , new {
                inventory = inventoryId
            });

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接