521

Consider the following code, where the BaseAddress defines a partial URI path.

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");
}

I expect this to perform a GET request to http://something.com/api/resource/7. But it doesn't.

After some searching, I find this question and answer: HttpClient with BaseAddress. The suggestion is to place / on the end of the 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");
}

It still doesn't work. Here's the documentation: HttpClient.BaseAddress What's going on here?

Community
  • 1
  • 1
Timothy Shields
  • 75,459
  • 18
  • 120
  • 173
  • 1
    Possible duplicate of [HttpClient with BaseAddress](http://stackoverflow.com/questions/20609118/httpclient-with-baseaddress) – George Lanetz Dec 10 '15 at 15:46
  • 3
    @ГеоргийЛанец The reverse duplicate has already been proposed. I wrote this question specifically because that other question was not written in a way that was very discoverable by people with the same problem, and I wrote the answer here because the answer over there left off an important point. – Timothy Shields Dec 10 '15 at 15:48
  • 1
    but this question is asked later – George Lanetz Dec 10 '15 at 15:50
  • 4
    @ГеоргийЛанец That's not how it works. Usually the most "canonical" question is the one that gets the duplicates pointing to it. That other question was about a single problem that user was having instead of reading like an FAQ. – Timothy Shields Dec 10 '15 at 15:52
  • 4
    @ГеоргийЛанец Also notice I reference that other question in this question, and I explain why the other question and answer are insufficient for solving the problem. – Timothy Shields Dec 10 '15 at 15:53

5 Answers5

1251

It turns out that, out of the four possible permutations of including or excluding trailing or leading forward slashes on the BaseAddress and the relative URI passed to the GetAsync method -- or whichever other method of HttpClient -- only one permutation works. You must place a slash at the end of the BaseAddress, and you must not place a slash at the beginning of your relative URI, as in the following example.

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");
}

Even though I answered my own question, I figured I'd contribute the solution here since, again, this unfriendly behavior is undocumented. My colleague and I spent most of the day trying to fix a problem that was ultimately caused by this oddity of HttpClient.

Timothy Shields
  • 75,459
  • 18
  • 120
  • 173
  • 1
    Sounds like someone from Microsoft has updated the link, http://msdn.microsoft.com/en-us/library/system.net.http.httpclient.baseaddress(v=vs.118).aspx I re-read what I typed and I did not mean to be "rude" as I just provided more facts about how Microsoft tries to document an API. The openness of SO is that it allows commenting, and everyone can share opinions. You provided your answer, so future readers can save time, and I commented with intention to save them more time in similar cases. Regards, – Lex Li May 03 '14 at 15:14
  • 2
    The trailing slash on a base address catches us all at some point. http://www.bizcoder.com/2009/02/24/the-mystery-of-the-trailing-slash-and-the-relative-url/ – Darrel Miller May 04 '14 at 17:03
  • 2
    not sure if its fixed, but you don't need the tailing slash for BaseAddress nor do you need to get rid of the slash at the beginning for relative path – Steve Oct 28 '16 at 15:32
  • 2
    @Steve I cannot confirm that. At least in my enviromnmet (PCL, Xamarin.Forms, Nancyfx) this fixed my problem. The client base url always said that it is correct but in response.RequestMessage.RequestUri it sitll was wrong. Changing the slashes solved it. – KCT Nov 29 '16 at 13:42
  • 10
    Thank you. That solved a problem I've been struggling with for most of two days now, between switching to Azure, back to IIS, and back to IIS Express, which most rudely ignores misplaced or extra forward slashes. Once set in the base class of my `RestClient`, it was almost invisible and got no attention at all, and I never saw the full url in at my breakpoints etc. – ProfK Dec 13 '16 at 14:32
  • 77
    I can confirm that this oddity (and this fix) is still relevant in .NET Core. Thanks for reducing my hair-pulling Timothy. – Nate Barbettini Mar 29 '17 at 19:32
  • 13
    This is because without trailing slash when it builds requests it drops last part. So it hits http://something.com/resource/7. If you set base address as http://something/com (doesn't matter if with or without trailing slash) it also doesn't matter if you put slash at the beginning of api/resource/7. Without trailing slash the last part of base address is treated like a file and dropped when bulding request. – Piotr Perak Jun 02 '17 at 07:52
  • 27
    Just a horrible implementation. Why don't they fix this? – timmkrause Oct 30 '18 at 16:14
  • 3
    Indeed that is plain unusal and quirky. You should allow no trailing slash at the base address and leading slash on path since the path is always with slash based on the standard. – Kat Lim Ruiz Oct 03 '20 at 21:26
  • 22
    OMG! Thank you! And eight years later with Visual Studio 2022 with .NET 6.0, we are still dealing with this braindead design! – Jonathan Wood Nov 22 '21 at 21:40
  • Oh my code! Thanks for sharing. Appreciate it. Btw, it's unbelievable it only works this way. – Karr May 23 '23 at 06:34
126

Reference Resolution is described by RFC 3986 Uniform Resource Identifier (URI): Generic Syntax. And that is exactly how it supposed to work. To preserve base URI path you need to add slash at the end of the base URI and remove slash at the beginning of relative URI.

If base URI contains non-empty path, merge procedure discards its last part (after last /). Relevant section:

5.2.3. Merge Paths

The pseudocode above refers to a "merge" routine for merging a relative-path reference with the path of the base URI. This is accomplished as follows:

  • If the base URI has a defined authority component and an empty path, then return a string consisting of "/" concatenated with the reference's path; otherwise
  • return a string consisting of the reference's path component appended to all but the last segment of the base URI's path (i.e., excluding any characters after the right-most "/" in the base URI path, or excluding the entire base URI path if it does not contain any "/" characters).

If relative URI starts with a slash, it is called an absolute-path relative URI. In this case the merge procedure ignores all base URI path. For more information check 5.2.2. Transform References section.

RJFalconer
  • 10,890
  • 5
  • 51
  • 66
Leonid Vasilev
  • 11,910
  • 4
  • 36
  • 50
  • 49
    Fine but client libraries such as HttpClient are supposed to shield us from esoteric implementation details like this. – Jamie Ide Jan 16 '20 at 15:11
  • 17
    It is great when answers cover reasons WHY something works this way, with relevant links and quotes. Even if this does not help with the issue from the question directly, it helps a lot. – Rast Jun 21 '20 at 16:18
  • 1
    that's why you have 10k stars and I have 174 silvers – knile Oct 21 '21 at 17:02
  • @JamieIde The only way it must shield you from this is to throw an error when you try to configure such malformed base url. – Dragas Jan 24 '23 at 16:11
16

if you are using httpClient.SendAsync() there is no string overload for giving relative Uris like the overloads for Get or other verb-specific methods.

But you can create relative Uri with giving UriKind.Relative as second param

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();
Iman
  • 17,932
  • 6
  • 80
  • 90
  • 2
    This only seems to work if your base address contains only the host address and domain and nothing else. For example, if you set the `BaseAddress` property of your `HttpClient` to `new Uri("https://mywebservice.com/api/public")` and create an `HttpRequestMessage` passing `RequestUri = new Uri("users/123456")` to the constructor, the URL used in the resulting request will be https://mywebservice.com/users/123456. This is annoying to me as all my requests use a common URL prefix (e.g. https://mywebservice.com/api/public) so it'd be nice to be able to set that as the base address. – Philip Stratford Feb 14 '23 at 10:31
4

I also came upon this same issue with BaseAddress. I decided to not use BaseAddress at all and simplest solution would be a simple one-liner addition:

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

Then your code would become:

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"));
}

I have not investigated pros and cons of using BaseAddress over this, but for me this works flawlessly. Hope this helps somebody.

kiko283
  • 490
  • 6
  • 15
0

Ran into a issue with the HTTPClient, even with the suggestions still could not get it to authenticate. Turns out I needed a trailing '/' in my relative path.

i.e.

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
            });
Tony
  • 17
  • 1
  • 1