7

I am using Google's GeoCoding API. I have two methods where one works and the other doesn't and I can't seem to figure out why:

string address = "1400,Copenhagen,DK";
string GoogleMapsAPIurl = "https://maps.googleapis.com/maps/api/geocode/json?address={0}&key={1}";
string GoogleMapsAPIkey = "MYSECRETAPIKEY";
string requestUri = string.Format(GoogleMapsAPIurl, address.Trim(), GoogleMapsAPIkey);

// Works fine                
using (var client = new HttpClient())
{
    using (HttpResponseMessage response = await client.GetAsync(requestUri))
    {
        var responseContent = response.Content.ReadAsStringAsync().Result;
        response.EnsureSuccessStatusCode();
    }
}

// Doesn't work
using (HttpClient client = new HttpClient())
{
    client.BaseAddress = new Uri("https://maps.googleapis.com/maps/api/", UriKind.Absolute);
    client.DefaultRequestHeaders.Add("key", GoogleMapsAPIkey);

    using (HttpResponseMessage response = await client.GetAsync("geocode/json?address=1400,Copenhagen,DK"))
    {
        var responseContent = response.Content.ReadAsStringAsync().Result;
        response.EnsureSuccessStatusCode();
    }
}

My last method with GetAsync where I am sending a query string doesn't work and I am in doubt why it is so. When I introduce BaseAddress on the client the GetAsync somehow doesn't send to the correct URL.

Hakan Dilek
  • 2,178
  • 2
  • 23
  • 35
Oliver Nilsen
  • 1,017
  • 2
  • 12
  • 32
  • 1
    Note: `using (HttpClient client = new HttpClient())` - [`HttpClient`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient?view=netframework-4.8#examples) is intended to be instantiated once per application, rather than per-use. – aepot Jul 19 '20 at 13:23
  • This is just a demo console app to show that I can't use a BaseAddress as I want to. – Oliver Nilsen Jul 19 '20 at 13:27

2 Answers2

5

I don't suggest adding API key into globals. Maybe you'll need to send some HTTP request outside of the API and the key will be leaked.

Here's the example that works.

using Newtonsoft.Json;
public class Program
{
    private static readonly HttpClient client = new HttpClient();
    private const string GoogleMapsAPIkey = "MYSECRETAPIKEY";

    static async Task Main(string[] args)
    {
        client.BaseAddress = new Uri("https://maps.googleapis.com/maps/api/");

        try
        {
            Dictionary<string, string> query = new Dictionary<string, string>();
            query.Add("address", "1400,Copenhagen,DK");
            dynamic response = await GetAPIResponseAsync<dynamic>("geocode/json", query);
            Console.WriteLine(response.ToString());
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex);
        }
        Console.ReadKey();
    }

    private static async Task<string> ParamsToStringAsync(Dictionary<string, string> urlParams)
    {
        using (HttpContent content = new FormUrlEncodedContent(urlParams))
            return await content.ReadAsStringAsync();
    }

    private static async Task<T> GetAPIResponseAsync<T>(string path, Dictionary<string, string> urlParams)
    {
        urlParams.Add("key", GoogleMapsAPIkey);
        string query = await ParamsToStringAsync(urlParams);
        using (HttpResponseMessage response = await client.GetAsync(path + "?" + query, HttpCompletionOption.ResponseHeadersRead))
        {
            response.EnsureSuccessStatusCode();
            string responseText = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject<T>(responseText);
        }
    }
}
aepot
  • 4,558
  • 2
  • 12
  • 24
  • Thx for the answer. I have this shorter form working fine now. The whole issue was passing the API key as a HTTP header, which Google's geocoding API doesn't accept. You have to send it as part of the query string. So this works for me: `client.BaseAddress = new Uri("https://maps.googleapis.com/maps/api/geocode/"); string requestURI = string.Format("json?address={0}&key={1}", address.Trim(), APIkey);` – Oliver Nilsen Jul 19 '20 at 20:11
  • @OliverNilsen You're welcome. Sure, that's shorter but my solution supports any collection of URL parameters not `address` only. It's scalable. I'm not sure that you'll use only `geocode/json` from that API. Then, `string.Format` here produce not proper URL-encoded string, it may fail with some not latin or special characters included. For example, you'll may easily break it with adding `&` or `=` to the `address` value. Just a warning but you may test and compare the resulting urls. Shorter not means better. – aepot Jul 19 '20 at 20:24
  • Hmm you might be right if I end up having & or = in the address. Can't I still use string.Format and just use one of the URI encoding methods and encode the address variable? – Oliver Nilsen Jul 23 '20 at 20:17
  • @OliverNilsen there's no so many bulletproof methods to encode the string into URL format correctly. One of them shown above, other one available with `HttpUtility` form `System.Web` namespace. But some developers don't like to include that namespace, I don't know why, thus I've shown the common one above. I have no idea why do you prefer to avoid the `ParamsToStringAsync` method approach. Btw, I'ts up to you. – aepot Jul 23 '20 at 20:41
4

Ciao, the problem is related with key parameter on URL. Change your code like this:

using (HttpClient client = new HttpClient())
{
   client.BaseAddress = new Uri("https://maps.googleapis.com/maps/api/");
   
   using (HttpResponseMessage response = await client.GetAsync("geocode/json?address=1400,Copenhagen,DK&key=" + GoogleMapsAPIkey))
    {
       var responseContent = response.Content.ReadAsStringAsync().Result;
       response.EnsureSuccessStatusCode();
    }
}

As google sheets said:

After you have an API key, your application can append the query parameter key=yourAPIKey to all request URLs. The API key is safe for embedding in URLs; it doesn't need any encoding.

Oliver Nilsen
  • 1,017
  • 2
  • 12
  • 32
Giovanni Esposito
  • 10,696
  • 1
  • 14
  • 30
  • I have tried that: client.BaseAddress = new Uri("https://maps.googleapis.com/maps/api/geocode/"); and calling GetSync with client.GetAsync("json?address=1400,Copenhagen,DK")) but it returns 403 – Oliver Nilsen Jul 19 '20 at 13:23
  • 1
    And what if client.GetAsync("json?address=1400,Copenhagen,DK&key=MYSECRETAPIKEY")) ? – Giovanni Esposito Jul 19 '20 at 13:30
  • I was just about to post that. This seems to work: client.BaseAddress = new Uri("https://maps.googleapis.com/maps/api/geocode/"); client.GetAsync("json?address=1400,Copenhagen,DK&key=myGoogleAPIkey") so as long as I am not setting the HTTP Header "key" then it does work. The key apparently needs to be sent as a query parameter. I thought that a HTTP header was standard way to send API keys. – Oliver Nilsen Jul 19 '20 at 13:36
  • I Update my answer. Good to ear that! – Giovanni Esposito Jul 19 '20 at 13:37
  • Strange that Google doesn't have the option to send the API key as a HTTP Header. Wouldn't it be more secure? – Oliver Nilsen Jul 19 '20 at 13:44
  • 1
    Yeah I tought the same. But reading [this](https://developers.google.com/sheets/api/guides/authorizing) they said "After you have an API key, your application can append the query parameter key=yourAPIKey to all request URLs. The API key is safe for embedding in URLs; it doesn't need any encoding." If google said is secure, must be secure :) – Giovanni Esposito Jul 19 '20 at 13:47
  • 1
    Checked twice. `UriKind.Absolute` has no effect int this exact case. – aepot Jul 19 '20 at 14:11
  • Ok, so the problem was just the key on url. Let me update the answer. – Giovanni Esposito Jul 19 '20 at 14:12
  • @OliverNilsen there's no difference on security where API key is located: in URL or in headers. HTTPS is enough to secure it. – aepot Jul 19 '20 at 15:42