Quantcast
Channel: Active questions tagged rest - Stack Overflow
Viewing all articles
Browse latest Browse all 3747

Unable to make a valid GET call with HttpClient

$
0
0

I'm trying to write a simple console app to communicate to my Honeywell thermostat. They offer a free-to-use REST API that's documented here: https://developer.honeywellhome.com/ . I am having trouble making a simple GET call right after authenticating, and I don't know what I'm doing wrong. I was hoping someone could help me here.

Summarized, my process consists of 3 steps:

  1. Register an app to get an AppID and a Secret (all good here).
  2. Authenticate with OAuth2 to get an access token (all good here).
  3. Call any REST API using the provided access token (problem is here).

Details

My console csproj is very simple:

<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>true</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>

1. Register an app to get an AppID and a Secret.

The app gets registed here, it's pretty standard: https://developer.honeywellhome.com/user/me/apps

For this example, let's assume Resideo gives me these values that I'll use later:

  • AppId: ABCD1234
  • Secret: WXYZ9876

2. Authenticate to get an access token.

The structure of the access token json response is described here: https://developer.honeywellhome.com/authorization-oauth2/apis/post/accesstoken

This is how I define the class for json deserialization:

internal class ResideoToken{    [JsonPropertyName("refresh_token_expires_in")]    public string RefreshTokenExpiration { get; set; } = string.Empty;    [JsonPropertyName("api_product_list")]    public string ApiProductList { get; set; } = string.Empty;    [JsonPropertyName("organization_name")]    public string OrganizationName { get; set; } = string.Empty;    [JsonPropertyName("developer.email")]    public string DeveloperEmail { get; set; } = string.Empty;    [JsonPropertyName("token_type")]    public string TokenType { get; set; } = string.Empty;    [JsonPropertyName("issued_at")]    public string IssuedAt { get; set; } = string.Empty;    [JsonPropertyName("client_id")]    public string ClientId { get; set; } = string.Empty;    [JsonPropertyName("access_token")]    public string AccessToken { get; set; } = string.Empty;    [JsonPropertyName("application_name")]    public string ApplicationName { get; set; } = string.Empty;    [JsonPropertyName("scope")]    public string Scope { get; set; } = string.Empty;    [JsonPropertyName("expires_in")]    public string ExpiresIn { get; set; } = string.Empty;    [JsonPropertyName("refresh_count")]    public string RefreshCount { get; set; } = string.Empty;    [JsonPropertyName("status")]    public string Status { get; set; } = string.Empty;}

And this is how I'm successfully authenticating:

string appId = "ABCD1234";string secret = "WXYZ9876";HttpClient client = new(){    BaseAddress = new Uri(uriString: "https://api.honeywell.com/", uriKind: UriKind.Absolute)};    KeyValuePair<string, string>[] encodedContentCollection =[    new("Content-Type", "application/x-www-form-urlencoded"),    new("grant_type", "client_credentials")];HttpRequestMessage request = new(HttpMethod.Post, "oauth2/accesstoken"){    Content = new FormUrlEncodedContent(encodedContentCollection)};string base64AppIdAndSecret = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{appId}:{secret}"));client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", base64AppIdAndSecret);HttpResponseMessage response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);response.EnsureSuccessStatusCode(); // Should throw if not 200-299Stream responseContentStream = await response.Content.ReadAsStreamAsync();ResideoToken token = await JsonSerializer.DeserializeAsync<ResideoToken>(responseContentStream, JsonSerializerOptions.Default) ??     throw new Exception("Could not deserialize response stream to a ResideoToken");

3. Call any REST API using the provided access token.

The simplest case I found is to get the list of locations and devices using GET method and passing one parameter: https://developer.honeywellhome.com/lyric/apis/get/locations

// I had this originally, but it was incorrect -> client.DefaultRequestHeaders.Add("Bearer", token.AccessToken);client.DefaultRequestHeaders.Authentication = new AuthenticationHeaderValue("Bearer", token.AccessToken);client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));// Base URL has already been established in the client// According to the instructions, the apikey is the AppIDHttpResponseMessage locationResponse = await client.GetAsync($"v2/locations?apikey={appId}");locationResponse.EnsureSuccessStatusCode(); // This is failing with 401 unauthorized// I am never able to reach thisstring result = await locationResponse.Content.ReadAsStringAsync();Console.WriteLine($"Locations: {result}");

As you can see, the GetAsync call fails with 401. This is the exception:

Unhandled exception. System.Net.Http.HttpRequestException: Response status code does not indicate success: 401 (Unauthorized).   at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()

This is strange, because if I print the base64 string generated by my code to console, copy it, and use it in a curl call, it succeeds:

$ curl -X GET --header "Authorization: Bearer AbCdEfGhIjKlMnOpQrStUvWxYz==" "https://api.honeywell.com/v2/locations?apikey=ABCD1234"# This prints a huge valid json file describing all the details of the locations and my devices as described in https://developer.honeywellhome.com/lyric/apis/get/locations

Questions

The 3rd step is where all the problems happen, so these are the things I'm unsure about:

  • Am I correctly reusing the access token string I got from the successful authentication?
  • Am I reusing the HttpClient correctly? It's my understanding that the recommendation is to keep using the same instance for the same group of requests identified with the same auth.
  • Am I setting the Bearer header in the right place as a default header, or should I manually create a request and set the header there? If the latter, how do I need to do it?
  • Am I setting the Accept media type of the default request header to a valid value of "application/json"? If not, where do I need to put it?
  • Do the default options originally set in the HttpClient when authenticating cause any issues with the subsequent request calls? In other words, do I need to clear the default headers for example?
  • Am I passing the correct url to the GetAsync call? It does not contain the base url.
  • Is it correct to set the GET parameter (apikey=1234ABCD) directly in the GetAsync url string? If not, what's the correct way?
  • Any suggestions on how to debug the 401 response, considering it did work when using curl?

Thanks in advance.

Edit: I fixed the line that sets the Bearer token but I am still getting the exact same 401 exception.


Viewing all articles
Browse latest Browse all 3747

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>