Проблема POST со службой json с использованием cpprest-sdk, взаимодействующей с jeasyui

Я разрабатываю веб-службу для обслуживания объектов json в асинхронном дереве jeasyui. . Мой HTML имеет следующее:

<ul id="tt" method="POST" class="easyui-tree" url="http://w.x.y.z:1024/testrest">
</ul>

Предположим, что w.x.y.z — это IP-адрес моего сервера. Согласно документации jeasyui для их службы PHP json, мне нужно вернуть массив объектов словаря с ключами id, text и state. Ладно, пока все хорошо. Я пытаюсь разработать службу json на С++, используя cpprest-sdk от Microsoft. Я скомпилировал и установил эту библиотеку на RHEL 7.2 и могу писать с ее помощью некоторые базовые службы. Проблема заключается (я думаю) в кодировке json, который отправляется обратно клиенту.

Вот полнофункциональный пример json-сервера, написанный с помощью cpprest-sdk, который обрабатывает POST-запросы и ответы с однократно заполненным массивом объектов словаря, которые соответствуют протоколу, ожидаемому jeasyui:

#include <cpprest/http_listener.h>
#include <cpprest/json.h>
#pragma comment(lib, "cpprestlib" )

using namespace web;
using namespace web::http;
using namespace web::http::experimental::listener;

#include <iostream>
#include <map>
#include <set>
#include <string>
using namespace std;

#define TRACE(msg)            wcout << msg 

void handle_request(http_request request, function<void(const json::value &, json::value &, bool)> action)
{
    json::value answer;

    TRACE("\nHandle_request\n");

    // Spit out the HTTP header to the console...
    const auto HeaderString = request.to_string();
    wcout << HeaderString.c_str() << endl;

    request
        .extract_json()
        .then([&answer, &action](pplx::task<json::value> task) {
            try
            {
                const auto & jvalue = task.get();
                if (!jvalue.is_null())
                {
                    action(jvalue, answer, false);
                }
                else
                {
                    action(jvalue, answer, true);
                }
            }
            catch (http_exception const & e)
            {
                wcout << "HTTP exception in handle_request: " << e.what() << endl;
            }
        })
        .wait();

    request.reply(status_codes::OK, answer);
}

void handle_post(http_request request)
{
    TRACE("\nHandle POST\n");

    handle_request(
        request, 
        [](const json::value & jvalue, json::value & answer, bool bNull)
        {

            const utility::string_t sID("id");
            const utility::string_t sText("text");
            const utility::string_t sState("state");


            if( bNull )
            {
                wcout << "jvalue must be null, setting some default values..." << endl;
                json::value group;              
                group[sID] = json::value::string("1");
                group[sText] = json::value::string("Hello");
                group[sState] = json::value::string("closed");
                answer[0] = group;
            }
            else
            {
                // To be written once the null case is sorted
            }
        }
    );
}

int main()
{
    uri_builder uri("http://w.x.y.z:1024/testrest");
    http_listener listener(uri.to_uri());

    listener.support(methods::POST, handle_post);
    try
    {
        listener
            .open()
            .then([&listener]()
                {
                    TRACE(L"\nStarting to listen\n");
                })
            .wait();

        while (true);
    }
    catch (exception const & e)
    {
        wcout << e.what() << endl;
    }

    return 0;
}

Это компилируется чисто, и я могу запустить службу на сервере Linux со следующим:

./testrest &
Starting to listen

Чтобы помочь в отладке, я использовал curl в качестве POST-клиента непосредственно на том же сервере Linux. Я использовал следующую команду для отправки запроса POST с нулевой длиной содержимого:

curl -i -X POST -H 'Content-Type: application/json' http://w.x.y.z:1024/testrest

Вывод из curl следующий:

HTTP/1.1 200 OK
Content-Length: 44
Content-Type: application/json

[{"id":"1","state":"closed","text":"Hello"}]

и консольные сообщения от моей службы такие:

Handle POST

Handle_request
POST /testrest HTTP/1.1
Accept: */*
Content-Type: application/json
Host: w.x.y.z:1024
User-Agent: curl/7.29.0


jvalue must be null, setting some default values...

Первые две строки соответствуют вызовам TRACE в коде. Средний раздел генерируется этим разделом кода:

// Spit out the HTTP header to the console...
const auto HeaderString = request.to_string();
wcout << HeaderString.c_str() << endl;

Основываясь на выводе curl, который представляет собой массив объектов словаря длиной ровно в одну запись, я ожидаю, что этот сервис будет отлично работать с jeasyui javascript на клиенте. Однако это не так. Мое асинхронное дерево никогда не заполняется, и я вообще ничего не вижу.

Я подозреваю, что что-то не так с кодировкой, поэтому я написал еще один сервис, используя web2py, чтобы проверить, будет ли он там работать. В моем контроллере default.py существует следующий код:

@service.json
def testweb2py():

    aRet=[]
    if request.post_vars.id is None:
        mydict={'id':'1','text':'Hello','state':'closed'}
        aRet.append(mydict)
    return aRet

после изменения моего клиента easyui-tree HTML, чтобы он указывал на URL-адрес web2py, он отлично заполняется, и я вижу узел. Я нажал на код web2py service.json с помощью curl, просто чтобы посмотреть, как результат может отличаться:

HTTP/1.1 200 OK
Date: Mon, 23 Jan 2017 18:17:17 GMT
Server: Apache/2.4.6 (Red Hat Enterprise Linux) OpenSSL/1.0.1e-fips mod_wsgi/3.4 Python/2.7.5
X-Powered-By: web2py
Expires: Mon, 23 Jan 2017 18:17:18 GMT
Pragma: no-cache
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Content-Length: 99
Content-Type: application/json; charset=utf-8

[{"text": "Hello", "state": "closed", "id": "1"}]

Помимо того, что заголовок содержимого совершенно другой, есть одна строка, которая, как я подозреваю, может иметь к этому какое-то отношение:

Content-Type: application/json; charset=utf-8

При вызове службы cpprest вывод заголовка из curl не включает в себя charset=utf-8. Если я сброшу вывод curl в файл с помощью переключателя -o, я не увижу четкой разницы между кодировкой. Единственное, что я вижу в формате json, — это дополнительные пробелы и порядок:

[{"text": "Hello", "state": "closed", "id": "1"}]    // web2py version
[{"id":"1","state":"closed","text":"Hello"}]         // cpprest version

Я не могу получить какой-либо контроль над порядком отправки словаря json, но я сомневаюсь, что это как-то связано с этим. Дополнительный пробел перед записью значения также кажется неуместным.

Я изучил документацию cpprest по адресу microsoft.github.io/cpprestsdk/index.html и не могу найти ничего, что касалось бы настройки выходной кодировки. Существует ряд переопределений для http_request::reply, которые включают параметры для установки типа содержимого, и я пошел по пути вызова их с жестко закодированными строками как для тела json, так и для типа содержимого json/application; charset=utf-8, все безрезультатно. . Я не понимаю, как эти переопределения можно использовать с объектами json::value в любом случае, поэтому я не думаю, что это оптимальный путь или жизнеспособное использование этой библиотеки cpprest.

Код javascript jeasyui кажется намеренно запутанным, и я мало верю в то, что смогу понять, что он делает с ответом на вызов POST. Может быть, кто-то, знакомый с jeasyui, может указать жизнеспособное средство для отладки асинхронного POST?

Пожалуйста помоги!


person tsm    schedule 23.01.2017    source источник


Ответы (1)


Так я понял, что происходит. Открыл консоль инструментов разработчика в Chrome и обнаружил следующее сообщение об ошибке:

XMLHttpRequest не может загрузить http://w.x.y.z:1024/testrest. В запрошенном ресурсе отсутствует заголовок «Access-Control-Allow-Origin». Таким образом, доступ к источнику 'http://w.x.y.z' запрещен.

Таким образом, это не имело никакого отношения к формату или кодировке моих данных json, а скорее к тому факту, что служба json была идентифицирована как ресурс, отличный от веб-сервера, сгенерировавшего клиентский HTML, которым он и является, и поэтому Chrome блокировал Это. Чтобы решить эту проблему, мне пришлось добавить некоторые поля заголовка в ответ, который я отправил обратно клиенту, а также добавить метод поддержки для обработки запросов OPTIONS от любого клиента, которому они могут понадобиться.

В моей функции main() я добавил:

listener.support(methods::OPTIONS, handle_options);

Затем я написал соответствующую функцию:

void handle_options(http_request request)
{
    http_response response(status_codes::OK);   
    response.headers().add(U("Allow"), U("POST, OPTIONS"));

    // Modify "Access-Control-Allow-Origin" header below to suit your security needs.  * indicates allow all clients
    response.headers().add(U("Access-Control-Allow-Origin"), U("*"));
    response.headers().add(U("Access-Control-Allow-Methods"), U("POST, OPTIONS"));
    response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type"));
    request.reply(response);
}

Наконец, мне пришлось добавить те же заголовки в request.reply в моем handle_request:

http_response response(status_codes::OK);

// Without these headers, the client browser will likely refuse the data and eat it
response.headers().add(U("Access-Control-Allow-Origin"), U("*"));
response.headers().add(U("Access-Control-Allow-Methods"), U("POST, OPTIONS"));
response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type"));
response.set_body(answer);
request.reply(response);    

Были и дополнительные проблемы... наиболее заметным из которых был тот факт, что класс jeasyui easyui_tree не отправляет данные POST с Content-Type application/json. Вместо этого он отправляет Content-Type application/x-www-form-urlencoded, поэтому мне пришлось добавить функцию для анализа кодировки URL-адреса с помощью libcurl. Это также означало замену request.extract_json() на request.extract_string() и соответствующие модификации соответствующих лямбда-функций, используемых cpprest.

Вот окончательный пример кода, возможно, он будет полезен другим, работающим в этих областях. Это полнофункциональный пример написания json-сервиса с cpprest (и не только в Linux), который отвечает на асинхронные POST-запросы от easyui_tree. Зависимости: boost, cpprest и libcurl-devel.

#include <boost/algorithm/string/replace.hpp>
#include <cpprest/http_listener.h>
#include <cpprest/json.h>
#include <curl/curl.h>
#pragma comment(lib, "cpprestlib" )

using namespace web;
using namespace web::http;
using namespace web::http::experimental::listener;

#include <iostream>
#include <map>
#include <vector>
#include <set>
#include <string>
using namespace std;

#define TRACE(msg)            wcout << msg 

void build_json( const utility::string_t &source, json::value &jvalue )
{
    // Use libcurl to unescape the POST body for us
    vector<string> splitvec;

    // We don't own the string created by curl_easy_unescape, so add a custom deleter
    string text = shared_ptr<char>( curl_easy_unescape( 0, source.c_str(), 0, 0 ), curl_free).get();

    // This works for this specific example of jeasyui, the class 'easyui-tree', which only passes id=... in the POST.  
    // Need custom handler to deal with more complicated data formats   
    boost::split( splitvec, text, boost::is_any_of("="));       
    if( splitvec.size() == 2 )
    {
        jvalue[splitvec.at(0)] = json::value::string(splitvec.at(1));
    }
}

void handle_request(http_request request, function<void(const json::value &, json::value &, bool)> action)
{
    json::value answer;

    auto objHeader = request.headers();
    auto sContentType = objHeader["Content-Type"];

    // Two cases: 
    // 1) The very first call from easyui_tree, when the HTML is first loaded, will make a zero-length POST with no 'Content-Type' in the header
    // 2) Subsequent calls from easyui_tree (e.g. when user opens a node) will have a Content-Type of 'application/x-www-form-urlencoded'
    // Nowhere does easyui_tree send json data in the POST, although it expects json in the reply
    if( sContentType.size() == 0 || 
        !strncasecmp( sContentType.c_str(), "application/x-www-form-urlencoded", strlen("application/x-www-form-urlencoded") ) )
    {
        request
            .extract_string()
            .then([&answer, &action](pplx::task<utility::string_t> task) {
                try
                {
                    const auto & svalue = task.get();
                    json::value jvalue;
                    if ( svalue.size() == 0 )
                    {
                        action(jvalue, answer, true);
                    }
                    else
                    {                       
                        build_json( svalue, jvalue );                       
                        action(jvalue, answer, false);
                    }
                }
                catch (http_exception const & e)
                {
                    wcout << "HTTP exception in handle_request: " << e.what() << endl;
                }
            })
            .wait();
    }
    else
    {
        // This Content-Type doesn't appear with easyui_tree, but perhaps it's still useful for future cases...
        if( !strncasecmp( sContentType.c_str(), "application/json", strlen("application/json") ) )
        {
            request
                .extract_json()
                .then([&answer, &action](pplx::task<json::value> task) {
                    try
                    {
                        const auto & jvalue = task.get();
                        if (!jvalue.is_null())
                        {
                            action(jvalue, answer, false);
                        }
                        else
                        {
                            action(jvalue, answer, true);
                        }
                    }
                    catch (http_exception const & e)
                    {
                        wcout << "HTTP exception in handle_request: " << e.what() << endl;
                    }
                })
                .wait();
        }
    }
    http_response response(status_codes::OK);

    // Without these headers, the client browser will likely refuse the data and eat it
    response.headers().add(U("Access-Control-Allow-Origin"), U("*"));
    response.headers().add(U("Access-Control-Allow-Methods"), U("POST, OPTIONS"));
    response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type"));
    response.set_body(answer);
    request.reply(response);    
}

void handle_options(http_request request)
{
    http_response response(status_codes::OK);   
    response.headers().add(U("Allow"), U("POST, OPTIONS"));

    // Modify "Access-Control-Allow-Origin" header below to suit your security needs.  * indicates allow all clients
    response.headers().add(U("Access-Control-Allow-Origin"), U("*"));
    response.headers().add(U("Access-Control-Allow-Methods"), U("POST, OPTIONS"));
    response.headers().add(U("Access-Control-Allow-Headers"), U("Content-Type"));
    request.reply(response);
}

void handle_post(http_request request)
{
    handle_request(
        request, 
        [](const json::value & jvalue, json::value & answer, bool bInitialize)
        {
            if( bInitialize )
            {
                // First time the tree is being loaded, first id will be 16, which will yield us 16 child nodes when it POSTs back
                json::value jreply;       
                jreply[U("id")] = json::value::string("16");
                jreply[U("text")] = json::value::string("Parent");
                jreply[U("state")] = json::value::string("closed");
                answer[0] = jreply;
            }
            else
            {
                // User has opened a node
                if( jvalue.type() == json::value::value_type::Object )
                {
                    if( jvalue.has_field( "id" ) )
                    {
                        auto & key = jvalue.at( "id" );
                        if( key.is_string() )
                        {
                            auto value = key.as_string();
                            int id = atoi(value.c_str());
                            stringstream ss;
                            ss << (id / 2);  // Each successive layer has half as many child nodes as the one prior
                            for( int i = 0; i < id; i++ )
                            {
                                json::value jreply;
                                jreply[U("id")] = json::value::string(ss.str());
                                jreply[U("text")] = json::value::string("Child");
                                jreply[U("state")] = json::value::string("closed");
                                answer[i] = jreply;
                            }
                        }
                    }
                }               
            }
        }
    );
}

int main()
{
    uri_builder uri("http://yourserver.com:1024/testrest");
    http_listener listener(uri.to_uri());

    listener.support(methods::POST, handle_post);
    listener.support(methods::OPTIONS, handle_options);
    try
    {
        listener
            .open()
            .then([&listener]()
                {
                    TRACE(L"\nStarting to listen\n");
                })
            .wait();

        while (true);
    }
    catch (exception const & e)
    {
        wcout << e.what() << endl;
    }
    return 0;
}

И, конечно же, соответствующий HTML-код, при условии, что все скрипты jeasyui указаны в заголовке:

<ul id="tt" method="POST" class="easyui-tree" url="http://yourserver.com:1024/testrest">
</ul>
person tsm    schedule 27.01.2017