Latest Nuget broken?

We've been using our own build of SDK so far - it allowed us to add some things and fix issues. I had another look at what's published on Github recently and was delighted to find that all my proposed changes had been integrated and that the project had been migrated to SDK style project. So I figured I'd give the Nuget another try. Only to fail miserably. I then downloaded the latest source code, compiled and ran my own code with my own build - which worked just fine.

So, I'm having two issues with the v224 nuget (both are fine if I make my own build)

  1. All requests are made without the access token. I see that the authentication request goes to /oauth/token instead of /token (the build on Github goes to /token). Both get a response, but on subsequent requests, the Nuget build does not send a token header whereas my own build does. Natuarlly, without the token header being present, every request I make after login gets me something like this:

{"message":"No authentication bearer token specified in authorization header.","code":"authentication.required","status":401,"contextId":"d4f3ee67-800d-4ec3-a8ae-1a2b9930dff9","details":[],"errors":[]}

Here's my authentication code

var accessTokenInfo = client.PostToken(clientId, clientSecret);

Initially I thought I must have missed a breaking change, but since things are fine when I make my own build, it seems that the published Nuget is from some other code.

  1. Setting a ConfigFilePath throws a Nullpointer exception. Here's how I'm using it
configuration = new Configuration();
client = configuration.ApiClient;
PureCloudRegionHosts region = PureCloudRegionHosts.eu_central_1;
client.setBasePath(region);
configuration.ConfigFilePath = "genesysConfig.json";

It's the last line that causes the problem. The stacktrace of the exception is

at PureCloudPlatform.Client.V2.Client.Configuration.ConfigurationParser.getJsonString(String section, String key)

And the contents of the genesysConfig.json file is

{
  "logging": {
    "log_level": "trace",
    "log_format": "text",
    "log_to_console": true,
    "log_file_path": "c:\\temp\\genesys.log",
    "log_response_body": true,
    "log_request_body": true
  },
  "retry": {
    "retry_wait_min": 3,
    "retry_wait_max": 10,
    "retry_max": 5
  },
  "reauthentication": {
    "refresh_access_token": true,
    "refresh_token_wait_max": 10
  },
  "general": {
    "live_reload_config": true,
    "host": "https://api.mypurecloud.de"
  }
}

So, what's going on here? I can of course keep using our own builds, but I'm at a loss why the published Nuget works differently from my own build (downloaded the source code from Github yesterday)

Hi Stephan,

But it compiled on my machine :). I don't have a good answer, but I will ask someone from my team to take a look. I am glad you have a work around with building locally and let me have someone dig into it.

Thanks,
John Carnell
Director, Developer Engagement

Hello,

I think that would help if you could share your code and project. Or at least enough code so that we can try to reproduce your issue.

I don't have a windows environment and I can't reproduce a nuget package install.
But I tried using the library in the nuget package with mono (Mac) and some of the lines you shared. And I didn't get any exception or request not having a token set.

I can try your code if that can work with mono (without nuget package install but using the PlatformClient library inside). And see I get the same errors or not (if it is a code conflict with the lib).

As a side note, I see you had a post regarding the use of the .Net SDK to connect to multiple environments in "parallel". No change has been made recently in the .Net SDK to help supporting this.

Also. The master branch of platform-client-sdk-dotnet does use /oauth/token now.
We just noticed recently that this endpoint was using /token. Although it is working, the proper path is /oauth/token. It was changed (and it is the case in the master branch of platform-client-sdk-dotnet).

Regards,

Given what you write, I suspect I must have fetched the source from my own fork rather than the original.

Just tested, and indeed that's the case. So I'll have to start anew to see what's broken.

I'm wondering though, any reason the changes in my fork are not acceptable? I converted to SDK style (which seems the way to go anyway for cross platform, went to a runtime agnostic version (netstandard2.0)), as a start, and then made various improvements. All my changes here. I fixed transparent access token re-fecthing when using client-credentials, added missing response logging in the error case in async API methods (it's in the sync version), and the Configuration.Default re-entrancy issue. All that code is already running in production - I just shuffled around my fork so that my changes are now in a branch, not main (hoping this'll make it easier to intregrate further upstream changes).

I'll try to set up a local copy now, reintegrate my changes to see what's going wrong.
The problem with code sharing is that I have my own lib to connect to Genesys, which wraps around the SDK. And that lib uses other private packages (as does my test project). So it takes some doing to unwind it all - it may be quicker to just figure out the problem in the source.

Alright, now with the correct latest source, I can see where things don't work out. Let's take the json file first.

The Nullpointer comes from this line in Configuration.cs

String gatewayHost = parser.GetString("gateway", "host");

In fact my genesysConfig.json (posted in my original post) file doesn't have this entry. Now, I'm curious about your approach to that file. When I have config files, I generally assume not every prop is filled, so I handle the case of missing values. Here it seems as if the file format was extended, and since my file is somewhat older (I guess from a time where there's no gateway entry), this causes the issue.

But when I look into the documentation in Github, the sample config does not have a gateway section either..

Now for the unauthenticated part, here's how I'm getting there. First I authenticate. I'm doing a machineless authentication, so I'm getting the token as already described:

var accessTokenInfo = client.PostToken(clientId, clientSecret);

If that is successful, I'm calling GetTokensMeAsync on the TokensApi. But it could really be any method on any API, the approach is the same

var api = GetTokenApi();
result.Result = await api.GetTokensMeAsync().ConfigureAwait(false);

private TokensApi GetTokenApi()
{
    lock (this)
    {
        tokenApi ??= new TokensApi(configuration);
        return tokenApi;
    }
}

It appears that instantiating TokensApi with the configuration object is no longer enough to get the authentication info. In fact, configuration.AuthTokenInfo contains an empty AuthTokenInfo object.

Why am I using a single configuration that I instantiate in the constructor of my Genesys Connector? If no configuration is provided, the default configuration is used. Which causes issues if you have concurrent connections because they default to using the default configuration (being static, there's one per application, what I need is one per connection to the Genesys Cloud). Plus, my application is written to be able to change settings at runtime. E.g. proxy. So at runtime, I'm using the single instance of the ApiClient, and set client.ClientOptions.Proxy. Likewise, I can update the credentials at runtime when I provide changed credentials (the app will automatically perform a new new Authentication operation (PostToken) when needed (when there's a change in ClientId or ClientSecret).

And why the Getter for the TokensApi Object (and I have the same approach for other APi objects)? I figured why instantiate Api Objects anew for every operation..

So, I created a new fork and ported my changes again. The fix for the re-entrancy issue of Configuration.Default took care of the missing token problem. But without the fix, the code in the master branch has a problem when using the various API objects that way I'm using them.

Little correction on issue #1 - it's just my debugger that tripped on the exception, it is caught inside the SDK, so all is fine there (at least when using the self-built version).

Do you mean the exception on reading the config file for "gateway" section?
If yes, good. I was writing the response back when you sent this.

As of the other part:

As I mentioned in my first answer, nothing has changed here for quite a while. I mean the sdk has not changed its behavior when managing multiple clients/configurations. The issue you are facing was already present with previous versions of the SDK.

I assume that you are creating 2 configurations (or more) and getting the ApiClient from there.
But when creating the Configuration with Configuration(), I think this will use the Default.ApiClient. So both configurations will end up with the same API client and as the AccessToken is stored at this level...
That's a constraint/limit in the SDK which has been there since the beginning.

I don't know if this would work for you - I mean if it can fit in your project and the way you are using ApiClient and Configuration - but here is a different way to initialize both that I just tried (and it seems it preserves the token/api client in both).

        Configuration configuration = new Configuration();
        ApiClient client =  new ApiClient(configuration);
        configuration.ApiClient = client;
        PureCloudRegionHosts region = PureCloudRegionHosts.eu_west_1;
        client.setBasePath(region);
        
        Configuration configuration2 = new Configuration();
        ApiClient client2 =  new ApiClient(configuration2);
        configuration2.ApiClient = client2;
        PureCloudRegionHosts region2 = PureCloudRegionHosts.us_east_1;
        client2.setBasePath(region2);

Using for login: var accessTokenInfo = client.PostToken(clientId, clientSecret);
And then: var apiInstance = new UsersApi(client.Configuration);

Hope this can work for you.

Regards,

Actually, for my tests right now, I'm just doing one connection. But in production, there's multiple connections. When I revert my fix for Configuration.Default re-entrancy and use your approach to instantiate the clients, the token is being sent after authentication.

I didn't change the instantiation of UsersApi (and other API projects) at first - so I was still using configuration / configuration2 respectively
(

var apiInstance = new UsersApi(configuration); 
var apiInstance2 = new UsersApi(configuration2)

). It seems that even though you are providing your own Configuration object to every instantiated ApiClient, client.Configuration gets you something else than configuration/configuration2. So, instead of going to the region I want (my customers are in eu_central_1), the API requests do go to the base url of us_east_1.

I found that without changing the API object instantiation, while API requests would now all have their token, they'd go to the wrong region. When I then compared configuration and client.Configuration, I saw that it's not the same object. And that seems wrong, no?

The problem is that client.setBasePath(region) sets client.ClientOptions.BaseUrl. But the basePath for the API requests inside any of the *Api objects (e.g. UsersApi) come from Configuration.ApiClient.ClientOptions - and since myApiInstance.Configuration isn't the same as myConfiguration, you end up using the default ClientOptions which has the default region of us_east_1. This behavior remains if you instantiate the API Objects with client.Configuration instead of configuration.

So to me that still looks like it would make sense to still have my re-entrancy fix, because that ensures that no Configuration objects are re-created.
It may run fine in your sequential code, but in my app, things are done concurrently (e.g. every of my X connections to Genesys does this pretty much at the same instance:

Configuration configuration = new Configuration();
ApiClient client =  new ApiClient(configuration);
configuration.ApiClient = client;
PureCloudRegionHosts region = PureCloudRegionHosts.eu_west_1;
client.setBasePath(region);
accessTokenInfo = client.PostToken(clientId, clientSecret);
var apiInstance = new UsersApi(client.Configuration);
// now do some stuff with apiInstance

so, what may work fine if you run it one after the other, may not run fine if you run it concurrently.

I did some further drilldown, when I instantiate two ApiClients as described above, we get more than three instances. The constructor of Configuration calls setApiClientUsingDefault(apiClient) (apiClient being null at the time), so inside setApiClientUsingDefault will instantiate a new Default.ApiClient. This is just where my re-entrancy fix starts to work and prevents this from happening.

Hello,

My purpose with the init code I posted was to find a way with existing SDK to initialize the two ApiClients independently (each of them having a distinct Configuration - client.Configuration).

The way I did the init was to find a way to go around this use of Default which has been in this SDK since beginning (just saying it is the way it was built a few years ago). Not saying it is ideal but to see if this could work with the way the SDK currently behaves.

Are you sure? I mean have you tried with current published SDK and this way of init for the ApiClient?
I know it is sequential but I did: init of ApiClient/Configuration and getting credentials for Region1), followed by the same for Region 2, and then creating and calling UsersApi for Client/Region1, followed by the same for Region2 and that worked ok. The entry point becomes the ApiClient.
I don't see how this would behave differently when running concurrently but I don't practice C# and .Net often (having a Mac).

Regards,

Seems I forgot the line

configuration.ApiClient = client;

with it, it does again work. But, it's a bit convoluted in my eyes. First we generate the Configuration, which in turn generates a new Default Config (which can happen multiple times if running things concurrently), AND a new ApiClient, then we create a new ApiClient again, and force that ApiClient on the Configuration. We call that a 'Schwanzbeisser' in German - 'dog chasing its tail' in English I guess.

I don't get why instantiating a new Configuration also instantiates an ApiClient to begin with. to me it seems more logical to instantiate a Configuration as the first step, instantiate the ApiClient with said Configuration, and instantiate the various APIs with an ApiClient (which in turns holds the Configuration). I guess the whole idea of the Default objects is to have some kind of shorthand when setting up things, but it really bites you in the ass when you have multiple connections and you work on them in parallel.

I think this SDK was initially built with the idea of being used in client apps with UI, and expecting a single Configuration/ApiClient and hasn't been changed much since then. I can't really comment on that part as it was done several years ago.

I may have an idea - a small change to mitigate this, without risking to break someone's existing code. I'll see if I can take a look at this next week. I need to write it and see if that indeed works.
I'll update this post once I have the answer.

Regards,

Hello,

There is a new release of the .Net SDK (version 225.0.0). I have added an optional parameter in the Configuration constructor - useDefaultApiClient (default: true - i.e. current behavior).
Setting the parameter to false will cause the constructor to avoid using and setting the Default.ApiClient.

With the code you had shared initially, it would just mean adding this parameter when calling the Configuration constructor.

configuration = new Configuration(useDefaultApiClient: false);
client = configuration.ApiClient;
PureCloudRegionHosts region = PureCloudRegionHosts.eu_central_1;
client.setBasePath(region);
configuration.ConfigFilePath = "genesysConfig.json";

Let me know if that works better for you/for your code.

Regards,

This topic was automatically closed 31 days after the last reply. New replies are no longer allowed.