2

Can anyone help? I'm a little confused

I'm using PostAsync to send messages to the Slack API. Code is below.

I'm trying to get the rate limiting code right, so after writing what I think is right I try to trigger the rate limiting by calling the code over and over again from a for loop (in this case, posting messages). The code catches the rate limit and seems to do what it should (wait until the limit as passed and then try again), but then I get an exception, generally but not always the next time it is called.

Exception is

Cannot access a disposed object.
Object name: 'System.Net.Http.StringContent'.

Source is System.Net.Http Stack Trace is:

   at System.Net.Http.HttpContent.CheckDisposed()
   at System.Net.Http.HttpContent.CopyToAsync(Stream stream, TransportContext context)
   at System.Net.Http.HttpClientHandler.GetRequestStreamCallback(IAsyncResult ar)
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at MyApp.MyForm.<SendMessageAsync>d__78.MoveNext() in
   C:\Users\James\source\repos\MyApp\MyApp\Form1.cs:line 1314

At this point I'm sure (well, 99% sure) the problem is in SendMessageAsync().

I thought it was the Thread.Sleep but when I remove that it happens less but still happens.

I've tried to track down when it is failing, and nearly every time it seemed to be from the PostAsync(), the next time it is called after the ratelimit code is run and the function exits; it may once have failed at JsonConvert.DeserializeObject(), not immediately after ratelimiting, but I can't be sure as I was in the early stages of debugging.

Can anyone help? It's driving me crazy...

Here's the code (forgive the primitive Exception handling, it's still in progress) - I could provide more context if need be.

    private static readonly HttpClient client = new HttpClient();

    // sends a slack message asynchronously
    public static async Task<Object> SendMessageAsync(string token, string APIMethod, Object msg, string contentType, Type returnType)
    {
        string content;
        switch (contentType)
        {
            case "application/json":
            default:
                // serialize method parameters to JSON
                content = JsonConvert.SerializeObject(msg);
                break;
            case "application/x-www-form-urlencoded":
                var keyValues = msg.ToKeyValue();
                if (keyValues != null)
                {
                    var formUrlEncodedContent = new FormUrlEncodedContent(keyValues);
                    content = await formUrlEncodedContent.ReadAsStringAsync();
                }
                else
                    content = "";
                break;
        }

        StringContent httpContent = new StringContent(content, Encoding.UTF8, contentType);

        // set token in authorization header
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

        try
        {
            Object messageResponse;
            bool doLoop;
            do
            {
                doLoop = false;
                // send message to API
                var response = await client.PostAsync("https://slack.com/api/" + APIMethod, httpContent);

                // fetch response from API
                var responseJson = await response.Content.ReadAsStringAsync();

                // convert JSON response to object
                messageResponse = JsonConvert.DeserializeObject(responseJson, returnType);

                dynamic genResponse = Convert.ChangeType(messageResponse, returnType);  // https://stackoverflow.com/questions/972636/casting-a-variable-using-a-type-variable
                if (genResponse.ok == false && genResponse.error == "ratelimited")
                {
                    if (response.Headers.RetryAfter != null && response.Headers.RetryAfter.Delta != null)
                    {
                        Thread.Sleep((TimeSpan)response.Headers.RetryAfter.Delta);
                        doLoop = true;
                    }
                }
            } while (doLoop);

            return messageResponse;
        }
        catch (Exception x) { throw x; }
    }
James Carlyle-Clarke
  • 830
  • 1
  • 12
  • 19

1 Answers1

3

You need to create a new StringContent per request. PostAsync will dispose the content.

When a request completes, HttpClient disposes the request content so the user doesn't have to. This also ensures that a HttpContent object is only sent once using HttpClient (similar to HttpRequestMessages that can also be sent only once).

Why do HttpClient.PostAsync and PutAsync dispose the content?

public static async Task<Object> SendMessageAsync(string token, string APIMethod, Object msg, string contentType, Type returnType)
    {
        string content;
        switch (contentType)
        {
            case "application/json":
            default:
                // serialize method parameters to JSON
                content = JsonConvert.SerializeObject(msg);
                break;
            case "application/x-www-form-urlencoded":
                var keyValues = msg.ToKeyValue();
                if (keyValues != null)
                {
                    var formUrlEncodedContent = new FormUrlEncodedContent(keyValues);
                    content = await formUrlEncodedContent.ReadAsStringAsync();
                }
                else
                    content = "";
                break;
        }

        // vvvv --- Move this line from here --- vvvv
        //StringContent httpContent = new StringContent(content, Encoding.UTF8, contentType);

        // set token in authorization header
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

        try
        {
            Object messageResponse;
            bool doLoop;
            do
            {
                doLoop = false;

                // vvvv --- To here --- vvv
                StringContent httpContent = new StringContent(content, Encoding.UTF8, contentType);

                // send message to API
                var response = await client.PostAsync("https://slack.com/api/" + APIMethod, httpContent);

                // fetch response from API
                var responseJson = await response.Content.ReadAsStringAsync();

                // convert JSON response to object
                messageResponse = JsonConvert.DeserializeObject(responseJson, returnType);

                dynamic genResponse = Convert.ChangeType(messageResponse, returnType);  // https://stackoverflow.com/questions/972636/casting-a-variable-using-a-type-variable
                if (genResponse.ok == false && genResponse.error == "ratelimited")
                {
                    if (response.Headers.RetryAfter != null && response.Headers.RetryAfter.Delta != null)
                    {
                        Thread.Sleep((TimeSpan)response.Headers.RetryAfter.Delta);
                        doLoop = true;
                    }
                }
            } while (doLoop);

            return messageResponse;
        }
        catch (Exception x) { throw x; }
    }
Jason
  • 1,505
  • 5
  • 9
  • good eye. so theoretically the first call will succeed and second always fails. – Joe_DM Sep 05 '20 at 08:17
  • Good eye, indeed! Solved it in one. Thanks @Jason, I can't tell you how much I appreciate that. Outstanding work, my friend! – James Carlyle-Clarke Sep 05 '20 at 09:50
  • Strangely, it didn't seem to fail when it looped back after ratelimit - it seemed to fail the time AFTER that loop back, when SendMessageAsync() had already finished and then was called again... – James Carlyle-Clarke Sep 05 '20 at 09:52
  • Glad to help. I cannot explain any reason as to why this would work with two iterations on the same content. In the source code it clearly disposes the content before completing the taskcompletionsource, check `SendAsync` (https://github.com/microsoft/referencesource/blob/master/System/net/System/Net/Http/HttpClient.cs). Is the code working now? – Jason Sep 05 '20 at 12:50
  • Seems to be working just fine. Yeah, very very strange, but it works, so it'll do me just fine. Maybe I'll play with it one day, see if I can work it out. – James Carlyle-Clarke Sep 06 '20 at 14:39