Getting 503 errors calling OpenRouteService through C# HttpClient call

Good Afternoon;

I’m running into an issue, and I am unsure as to why I am getting a 503 Service Unavailable error.

For starters, I know I’m hitting the API endpoint (https://api.openrouteservice.org/v2/isochrones/driving-car) and that the endpoint is active because I can see the errors in the dashboard side (14 errors today and counting trying to sort this out myself).

I also know that the API key is correct and being received because I would get 403 errors before I put in a proper API key, so I know I’m hitting the right spot.

Now, as for the problem, I think based on my bloodied and bleary forehead that the issue lies in the parameters I’m trying to send to the API which are a straight copy from the Javascript SDK, so I’ll go into details regarding how exactly I’m doing that.

First of all though, I’m operating on a modification of the Javascript SDK source files, stripping out the NPM components (Bluebird and Superagent) and instead pushing the negotiation portion between servers to the C# back-end to deal with the CORS issue that comes from trying to send raw AJAX to another server.

That looks something like this (in the _layout.cshtml partial that loads to every page that uses it):

<script type="module" src="~/lib/ORS/src/OrsBase.js"></script>
    <script type="module">
        import OrsUtil from "@Url.Content("~/lib/ORS/src/OrsUtil.js")";
        import OrsInput from "@Url.Content("~/lib/ORS/src/OrsInput.js")";
        import OrsGeocode from "@Url.Content("~/lib/ORS/src/OrsGeocode.js")";
        import OrsIsochrones from "@Url.Content("~/lib/ORS/src/OrsIsochrones.js")";
        import OrsDirections from "@Url.Content("~/lib/ORS/src/OrsDirections.js")";
        import OrsMatrix from "@Url.Content("~/lib/ORS/src/OrsMatrix.js")";
        import OrsPois from "@Url.Content("~/lib/ORS/src/OrsPois.js")";
        import OrsElevation from "@Url.Content("~/lib/ORS/src/OrsElevation.js")";
        import OrsOptimization from "@Url.Content("~/lib/ORS/src/OrsOptimization.js")";

        const Openrouteservice = {
            Util: OrsUtil,
            Input: OrsInput,
            Geocode: OrsGeocode,
            Isochrones: OrsIsochrones,
            Directions: OrsDirections,
            Matrix: OrsMatrix,
            Pois: OrsPois,
            Elevation: OrsElevation,
            Optimization: OrsOptimization
        };

        if (typeof module === "object" && typeof module.exports === "object") {
            module.exports = Openrouteservice;
        } else if (typeof define === "function" && define.amd) {
            define(Openrouteservice);
        }
        if (typeof window !== "undefined") {
            window.Openrouteservice = Openrouteservice;
        }

        export {
            Openrouteservice as default
        };
    </script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)

Then, in my Index.cshtml, I am throwing the Isochrone test content from the Javascript SDK, which looks something like this:

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>

    <h1>ORS-js lib examples</h1>
    <section>
        <h2>Isochrones</h2>
        <div id="isochrones"></div>

    </section>
</div>

@section Scripts{
    <script type='module'>
        window.onload = function () {
            const node = document.getElementById("isochrones")

            const Isochrones = new Openrouteservice.Isochrones({
                api_key: apiKey
            })

            Isochrones.calculate({
                profile: 'driving-car',
                locations: [[8.681495, 49.41461], [8.686507, 49.41943]],
                range: [300, 200],
                area_units: 'km'
            })
                .then(function (json) {
                    let response = JSON.stringify(json, null, "\t")
                    console.log(response);
                    response = response.replace(/(\n)/g, '<br>');
                    response = response.replace(/(\t)/g, '&nbsp;&nbsp;');
                    node.innerHTML = "<h3>Response</h3><p>" + response + "</p>";
                })
                .catch(function (err) {
                    let response = JSON.stringify(err, null, "\t")
                    console.log(response);
                    response = response.replace(/(\n)/g, '<br>');
                    response = response.replace(/(\t)/g, '&nbsp;&nbsp;');
                    node.innerHTML = "<h3>Response</h3><p>" + response + "</p>";
                })
        }
    </script>
}

Then, I altered the OrsBase.js file to stop calling it’s own cross-domain call and instead call an internal function (because of CORS and removing the NPM requirements) (NOTE: I am only calling console.logs at the moment because I want to see the result data before I start working on shunting it back to where it goes using resolve / reject)

createRequest(body, resolve, reject) {
        let url = orsUtil.prepareUrl(this.argsCache)
        if (this.argsCache[Constants.propNames.service] === 'pois') {
            url += url.indexOf('?') > -1 ? '&' : '?'
        }

        const timeout = this.defaultArgs[Constants.propNames.timeout] || 10000

        //Calling C# level section to handle CORS issues

        $.ajax({
            type: "POST",
            url: baseUrl + "Home/getORS",
            async: true,
            dataType: 'json',
            data: {
                "url": url,
                "args": JSON.stringify(this.customHeaders),
                "body": JSON.stringify(body)
            },
            timeout: timeout,
            success: function (data) {
                console.log(data);
            },
            error: function (jqXHR, textStatus, errorThrown) {
                console.error(jqXHR);
            },
            complete: function (jqXHR, textStatus) {
                console.log(jqXHR);
            }
        });
    }

The actual API call is handled currently in the controller, but will eventually be moved into a model for production, that looks something like this:

public async Task<IActionResult> getORS(string url, string args, string body)
        {
            Uri? uriResult;
            bool result = Uri.TryCreate(url, UriKind.Absolute, out uriResult) && (uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);

            if (!result)
            {
                return Json("");
            }

            HttpClient client = new HttpClient();

            WebApplicationBuilder builder = WebApplication.CreateBuilder();
            string apiKey = builder.Configuration.GetSection("ORS")["apiKey"];

            HttpRequestMessage request = new HttpRequestMessage
            {
                Method = HttpMethod.Post,
                RequestUri = uriResult,
                Headers = {
                    { HttpRequestHeader.Authorization.ToString(), apiKey },
                    { HttpRequestHeader.Accept.ToString(), "application/json" },
                },
                Content = new StringContent(body, System.Text.Encoding.UTF8, "application/json")
            };

            if (args != "[]")
            {
                foreach (KeyValuePair<string, string> val in JsonConvert.DeserializeObject<Dictionary<string, string>>(args))
                {
                    request.Headers.Add(val.Key, val.Value);
                }
            }

            var response = await client.SendAsync(request).ConfigureAwait(false);
            try
            {
                response.EnsureSuccessStatusCode();
                return Json(await response.Content.ReadAsStringAsync().ConfigureAwait(false));
            }
            catch (Exception ex)
            {
                var returnVal = new { returnStatusCode = response.StatusCode, reason = response.ReasonPhrase };
                return Json(returnVal);
            }
        }

So, as you can see, I am calling the Isochrone builder, going through and polling the data, and when I hit the HTTPRequest, it’s firing back the 503.

Additionally, when there was another value (at one point I had altitude and manually re-created the object that is being passed up from the test front-end) for altitude which caused the API to return a 406 error because it wasn’t expecting the variable.

From the console in the browser, the body, before it is turned into a JSON string, looks like this:

{
    "locations": [
        [
            8.681495,
            49.41461
        ],
        [
            8.686507,
            49.41943
        ]
    ],
    "range": [
        300,
        200
    ],
    "area_units": "km"
}

And that comes through to the controller as a string like so:

{"locations":[[8.681495,49.41461],[8.686507,49.41943]],"range":[300,200],"area_units":"km"}

So, the question is, what is missing, misconfigured, or wrong with this data I’m sending as the body of the HttpRequestMessage content?

Alright, well I managed to sort the issue… partially…

So I checked again today after reading the other 503 error thread and today it started returning error code 406 repeatedly.

So I started to dig into the API documentation, and after a few more haul-offs on the test example I noticed something I didn’t expect: While it is technically using the JSON notation spec, the return type is not JSON, but instead GEO+JSON.

What does this mean for my nonsense above? To get it to talk to the API the way I’m currently doing via C# you need to change two lines in my HTTPClient Code (from the controller / model), both locations that refer to the content type as “application/json” to “application/geo+json” and it’ll start returning 200 codes with all the data needed to move on to the next step.

Hope this helps some other poor developer who finds themselves in these same waters in the future!

2 Likes