Search code examples
c++httpwinhttp

WinHTTP async context invalid in callback


I am trying to use WinHTTP to make asynchronous calls to a server but am having problems:

  1. The context in the callback seems to be incorrect when casting and accessing the struct values such as the request id.
  2. The WinHttpReceiveResponse always returns an error code of 6 using GetLastError() which is a system error of invalid handle which I suspect is related to point 1.

The only reason I can think of for my context to no longer be valid is if it is going out of scope but I cannot see how this is happening. Any insight would be much appreciated.

My context struct is

struct connection_context
{
    int request_id;
    HANDLE read_complete;
    DWORD timeout;
    HINTERNET request_handle;
    std::string request_payload;
    std::string read_buffer;
    std::string response_payload;
};

My WinHTTP callback is

void async_callback(HINTERNET hInternet, DWORD_PTR dwContext, DWORD dwInternetStatus, LPVOID lpvStatusInformation, DWORD dwStatusInformationLength)
{
    if (dwContext != 0)
    {
        connection_context* ctx = (connection_context*)dwContext;

        switch(dwInternetStatus)
        {
            case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
                if(!WinHttpReceiveResponse(ctx->request_handle, NULL))
                {
                    printf("Request %d Error in receive response, code: %d\n", ctx->request_id, GetLastError());
                    SetEvent(ctx->read_complete);
                }
                break;
            case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
            {
                DWORD statusCode = 0;
                DWORD statusCodeSize = sizeof(DWORD);

                if (!WinHttpQueryHeaders(ctx->request_handle, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusCodeSize, WINHTTP_NO_HEADER_INDEX))
                {
                    printf("Request %d failed to query headers\n", ctx->request_id);
                    SetEvent(ctx->read_complete);
                }

                if (HTTP_STATUS_OK != statusCode)
                {
                    printf("Request %d bad status\n", ctx->request_id);
                    SetEvent(ctx->read_complete);
                }

                if(!WinHttpQueryDataAvailable(ctx->request_handle, NULL))
                {
                    printf("Reques %d bad query data available\n", ctx->request_id);
                    SetEvent(ctx->read_complete);
                }
                break;
            }
            case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE:
                if (*((LPDWORD)lpvStatusInformation) != 0)
                {
                    DWORD buffer_size = *((LPDWORD)lpvStatusInformation) + 1;
                    ctx->read_buffer = std::string(buffer_size, '\0');

                    if (!WinHttpReadData(ctx->request_handle, &ctx->read_buffer[0], ctx->read_buffer.size(), 0))
                    {
                        printf("Request %d bad subsequent read data\n", ctx->request_id);
                        SetEvent(ctx->read_complete);
                    }
                }
                else
                {
                    printf("Request %d read complete with size %zd\n", ctx->request_id, ctx->response_payload.size());
                    SetEvent(ctx->read_complete);
                }
                break;
            case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
                if (dwStatusInformationLength != 0)
                {
                    ctx->response_payload.append(ctx->read_buffer.c_str(), dwStatusInformationLength);
                    ctx->read_buffer.clear();

                    if(!WinHttpQueryDataAvailable(ctx->request_handle, NULL))
                    {
                        printf("Request %d bad query data available\n", ctx->request_id);
                        SetEvent(ctx->read_complete);
                    }
                }
                break;        
        }
    }
}

The function that sets-up the connection and sends the requests and waits for their completion is

void fetch_status(std::string login_result, LPCWSTR pswzServerName, WORD port, LPCWSTR lpszVersion, LPCWSTR lpszReferrer, LPCWSTR* lplpszAcceptTypes, DWORD dwFlags, std::unordered_map<int, std::string>* results)
{
    HINTERNET session = WinHttpOpen(L"Steam", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC);

    if (!session)
    {
        printf("Failed to make session\n");
        return;
    }

    WinHttpSetStatusCallback(session, async_callback, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, 0);

    HINTERNET connect = WinHttpConnect(session, pswzServerName, port, 0);

    if (!connect)
    {
        printf("Failed to make connect\n");
        WinHttpCloseHandle(session);
        return;
    }

    std::vector<connection_context> contexts;

    std::string token = login_result.substr(60, 19) + login_result.substr(2, 14);
    token = to_hex(token);

    for (auto i = 0; i < ggs_get_aobs.size(); i++)
    {
        connection_context ctx;
        contexts.push_back(ctx);
        contexts.back().request_id = i;
        contexts.back().timeout = 5000; // 5 seconds
        contexts.back().request_payload = generate_stat_get_request(token, ggs_get_aobs[i]);
        contexts.back().request_handle = WinHttpOpenRequest(connect, L"POST", L"/api/statistics/get", NULL, WINHTTP_NO_REFERER, lplpszAcceptTypes, WINHTTP_FLAG_SECURE);

        if (!contexts.back().request_handle)
        {
            printf("Failed to open request\n");
            continue;
        }

        int headers_added = 0;

        if(WinHttpAddRequestHeaders(contexts.back().request_handle, L"Connection: keep-alive\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE))
        {
            headers_added++;
        }

        if(WinHttpAddRequestHeaders(contexts.back().request_handle, L"Content-Type: application/x-www-form-urlencoded\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE))
        {
            headers_added++;
        }

        if(WinHttpAddRequestHeaders(contexts.back().request_handle, L"Cache-Control: no-cache\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE))
        {
            headers_added++;
        }

        if(WinHttpAddRequestHeaders(contexts.back().request_handle, L"Cookie: theme=theme-dark\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE))
        {
            headers_added++;
        }

        if (WinHttpAddRequestHeaders(contexts.back().request_handle, L"User-Agent: Steam\r\n", -1L, WINHTTP_ADDREQ_FLAG_ADD | WINHTTP_ADDREQ_FLAG_REPLACE))
        {
            headers_added++;
        }

        if (headers_added < 5)
        {
            printf("Failed to add headers\n");
            continue;
        }

        if(!WinHttpSendRequest(contexts.back().request_handle, WINHTTP_NO_ADDITIONAL_HEADERS, -1L, 
            &contexts.back().request_payload[0], contexts.back().request_payload.size() + 1, contexts.back().request_payload.size() + 1, (DWORD_PTR)&contexts.back()))
        {
            printf("Failed to send request\n");
            continue;
        }

        printf("Sent async http request %d, otional(size:%zd)\n", i, contexts.back().request_payload.size() + 1);
    }

    for (auto i = 0; i < contexts.size(); i++)
    {
        WaitForSingleObject(contexts[i].read_complete, contexts[i].timeout);
        WinHttpCloseHandle(contexts[i].request_handle);
    }

    WinHttpCloseHandle(connect);
    WinHttpCloseHandle(session);
}

Solution

  • On each iteration of the loop that calls WinHttpSendRequest(), setting the context of each request, the call to contexts.push_back() has the potential to reallocate the vector's inner array, thus invalidating all previous connection_context* pointers you are using in earlier requests.

    To avoid that reallocation, you need to pre-size the array before entering the loop.

    Either like this:

    std::vector<connection_context> contexts(ggs_get_aobs.size());
    
    ...
    
    size_t i = 0;
    for (auto &ctx : contexts)
    {
        ctx.request_id = i;
        ...
        ctx.request_handle = WinHttpOpenRequest(...);
        ...
        WinHttpSendRequest(ctx.request_handle, ..., reinterpret_cast<DWORD_PTR>(&ctx))
        ...
        ++i;
    }
    

    Or, like this:

    std::vector<connection_context> contexts;
    contexts.reserve(ggs_get_aobs.size());
    
    ...
    
    for (auto i = 0; i < ggs_get_aobs.size(); ++i)
    {
        contexts.emplace_back();
        auto &ctx = contexts.back();
        /* or, in C++17 and later...
        auto &ctx = contexts.emplace_back();
        */
    
        ctx.request_id = i;
        ...
        ctx.request_handle = WinHttpOpenRequest(...);
        ...
        WinHttpSendRequest(ctx.request_handle, ..., reinterpret_cast<DWORD_PTR>(&ctx))
        ...
    }