RESTful API requests using Qt/C++ for Linux, Mac OSX, MS Windows
In a recent article we showed how HTTP requests are formed in low level. We demonstrated how to create a simple HTTP GET request, a URL encoded POST request, and an upload request (multipart POST HTTP request).
This article has working examples on how to create those HTTP requests programmatically using Qt for Linux, Mac OSX or MS Windows.
The next sections of this article include the code that does the job, and then a couple of examples that demonstrate ways to create different types of HTTP requests.
The code
Create a new C++ Class with the name HttpRequestWorker with a base class of QObject.
Copy the following code in the httprequestworker.h file.
#ifndef HTTPREQUESTWORKER_H #define HTTPREQUESTWORKER_H #include <QObject> #include <QString> #include <QMap> #include <QNetworkAccessManager> #include <QNetworkReply> enum HttpRequestVarLayout {NOT_SET, ADDRESS, URL_ENCODED, MULTIPART}; class HttpRequestInputFileElement { public: QString variable_name; QString local_filename; QString request_filename; QString mime_type; }; class HttpRequestInput { public: QString url_str; QString http_method; HttpRequestVarLayout var_layout; QMap<QString, QString> vars; QList<HttpRequestInputFileElement> files; HttpRequestInput(); HttpRequestInput(QString v_url_str, QString v_http_method); void initialize(); void add_var(QString key, QString value); void add_file(QString variable_name, QString local_filename, QString request_filename, QString mime_type); }; class HttpRequestWorker : public QObject { Q_OBJECT public: QByteArray response; QNetworkReply::NetworkError error_type; QString error_str; explicit HttpRequestWorker(QObject *parent = 0); QString http_attribute_encode(QString attribute_name, QString input); void execute(HttpRequestInput *input); signals: void on_execution_finished(HttpRequestWorker *worker); private: QNetworkAccessManager *manager; private slots: void on_manager_finished(QNetworkReply *reply); }; #endif // HTTPREQUESTWORKER_H
Copy the following code in the httprequestworker.cpp file.
#include "httprequestworker.h" #include <QDateTime> #include <QUrl> #include <QFileInfo> #include <QBuffer> HttpRequestInput::HttpRequestInput() { initialize(); } HttpRequestInput::HttpRequestInput(QString v_url_str, QString v_http_method) { initialize(); url_str = v_url_str; http_method = v_http_method; } void HttpRequestInput::initialize() { var_layout = NOT_SET; url_str = ""; http_method = "GET"; } void HttpRequestInput::add_var(QString key, QString value) { vars[key] = value; } void HttpRequestInput::add_file(QString variable_name, QString local_filename, QString request_filename, QString mime_type) { HttpRequestInputFileElement file; file.variable_name = variable_name; file.local_filename = local_filename; file.request_filename = request_filename; file.mime_type = mime_type; files.append(file); } HttpRequestWorker::HttpRequestWorker(QObject *parent) : QObject(parent), manager(NULL) { qsrand(QDateTime::currentDateTime().toTime_t()); manager = new QNetworkAccessManager(this); connect(manager, SIGNAL(finished(QNetworkReply*)), this, SLOT(on_manager_finished(QNetworkReply*))); } QString HttpRequestWorker::http_attribute_encode(QString attribute_name, QString input) { // result structure follows RFC 5987 bool need_utf_encoding = false; QString result = ""; QByteArray input_c = input.toLocal8Bit(); char c; for (int i = 0; i < input_c.length(); i++) { c = input_c.at(i); if (c == '\\' || c == '/' || c == '\0' || c < ' ' || c > '~') { // ignore and request utf-8 version need_utf_encoding = true; } else if (c == '"') { result += "\\\""; } else { result += c; } } if (result.length() == 0) { need_utf_encoding = true; } if (!need_utf_encoding) { // return simple version return QString("%1=\"%2\"").arg(attribute_name, result); } QString result_utf8 = ""; for (int i = 0; i < input_c.length(); i++) { c = input_c.at(i); if ( (c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ) { result_utf8 += c; } else { result_utf8 += "%" + QString::number(static_cast<unsigned char>(input_c.at(i)), 16).toUpper(); } } // return enhanced version with UTF-8 support return QString("%1=\"%2\"; %1*=utf-8''%3").arg(attribute_name, result, result_utf8); } void HttpRequestWorker::execute(HttpRequestInput *input) { // reset variables QByteArray request_content = ""; response = ""; error_type = QNetworkReply::NoError; error_str = ""; // decide on the variable layout if (input->files.length() > 0) { input->var_layout = MULTIPART; } if (input->var_layout == NOT_SET) { input->var_layout = input->http_method == "GET" || input->http_method == "HEAD" ? ADDRESS : URL_ENCODED; } // prepare request content QString boundary = ""; if (input->var_layout == ADDRESS || input->var_layout == URL_ENCODED) { // variable layout is ADDRESS or URL_ENCODED if (input->vars.count() > 0) { bool first = true; foreach (QString key, input->vars.keys()) { if (!first) { request_content.append("&"); } first = false; request_content.append(QUrl::toPercentEncoding(key)); request_content.append("="); request_content.append(QUrl::toPercentEncoding(input->vars.value(key))); } if (input->var_layout == ADDRESS) { input->url_str += "?" + request_content; request_content = ""; } } } else { // variable layout is MULTIPART boundary = "__-----------------------" + QString::number(QDateTime::currentDateTime().toTime_t()) + QString::number(qrand()); QString boundary_delimiter = "--"; QString new_line = "\r\n"; // add variables foreach (QString key, input->vars.keys()) { // add boundary request_content.append(boundary_delimiter); request_content.append(boundary); request_content.append(new_line); // add header request_content.append("Content-Disposition: form-data; "); request_content.append(http_attribute_encode("name", key)); request_content.append(new_line); request_content.append("Content-Type: text/plain"); request_content.append(new_line); // add header to body splitter request_content.append(new_line); // add variable content request_content.append(input->vars.value(key)); request_content.append(new_line); } // add files for (QList<HttpRequestInputFileElement>::iterator file_info = input->files.begin(); file_info != input->files.end(); file_info++) { QFileInfo fi(file_info->local_filename); // ensure necessary variables are available if ( file_info->local_filename == NULL || file_info->local_filename.isEmpty() || file_info->variable_name == NULL || file_info->variable_name.isEmpty() || !fi.exists() || !fi.isFile() || !fi.isReadable() ) { // silent abort for the current file continue; } QFile file(file_info->local_filename); if (!file.open(QIODevice::ReadOnly)) { // silent abort for the current file continue; } // ensure filename for the request if (file_info->request_filename == NULL || file_info->request_filename.isEmpty()) { file_info->request_filename = fi.fileName(); if (file_info->request_filename.isEmpty()) { file_info->request_filename = "file"; } } // add boundary request_content.append(boundary_delimiter); request_content.append(boundary); request_content.append(new_line); // add header request_content.append(QString("Content-Disposition: form-data; %1; %2").arg( http_attribute_encode("name", file_info->variable_name), http_attribute_encode("filename", file_info->request_filename) )); request_content.append(new_line); if (file_info->mime_type != NULL && !file_info->mime_type.isEmpty()) { request_content.append("Content-Type: "); request_content.append(file_info->mime_type); request_content.append(new_line); } request_content.append("Content-Transfer-Encoding: binary"); request_content.append(new_line); // add header to body splitter request_content.append(new_line); // add file content request_content.append(file.readAll()); request_content.append(new_line); file.close(); } // add end of body request_content.append(boundary_delimiter); request_content.append(boundary); request_content.append(boundary_delimiter); } // prepare connection QNetworkRequest request = QNetworkRequest(QUrl(input->url_str)); request.setRawHeader("User-Agent", "Agent name goes here"); if (input->var_layout == URL_ENCODED) { request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); } else if (input->var_layout == MULTIPART) { request.setHeader(QNetworkRequest::ContentTypeHeader, "multipart/form-data; boundary=" + boundary); } if (input->http_method == "GET") { manager->get(request); } else if (input->http_method == "POST") { manager->post(request, request_content); } else if (input->http_method == "PUT") { manager->put(request, request_content); } else if (input->http_method == "HEAD") { manager->head(request); } else if (input->http_method == "DELETE") { manager->deleteResource(request); } else { QBuffer buff(&request_content); manager->sendCustomRequest(request, input->http_method.toLatin1(), &buff); } } void HttpRequestWorker::on_manager_finished(QNetworkReply *reply) { error_type = reply->error(); if (error_type == QNetworkReply::NoError) { response = reply->readAll(); } else { error_str = reply->errorString(); } reply->deleteLater(); emit on_execution_finished(this); }
The controller class should contain one or more places that call the worker unit and one event handler function. The event handler is required because this example's code makes asynchronous HTTP calls in order for the main thread to keep being responsive and not freeze.
In our example the main class of the application is called MainWindow and it contains a button which triggers the examples that follow.
Here is the code for the header file of the MainWindow class (mainwindow.h).
#ifndef MAINWINDOW_H #define MAINWINDOW_H #include <QMainWindow> #include "httprequestworker.h" namespace Ui { class MainWindow; } class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = 0); ~MainWindow(); private: Ui::MainWindow *ui; private slots: void on_pushButton_clicked(); void handle_result(HttpRequestWorker *worker); }; #endif // MAINWINDOW_H
Here is the code for the main file of the MainWindow class (mainwindow.cpp).
#include "mainwindow.h" #include "ui_mainwindow.h" #include <QNetworkReply> #include <QMessageBox> MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui->setupUi(this); } MainWindow::~MainWindow() { delete ui; } void MainWindow::on_pushButton_clicked() { // trigger the request - see the examples in the following sections } void MainWindow::handle_result(HttpRequestWorker *worker) { QString msg; if (worker->error_type == QNetworkReply::NoError) { // communication was successful msg = "Success - Response: " + worker->response; } else { // an error occurred msg = "Error: " + worker->error_str; } QMessageBox::information(this, "", msg); }
There are a couple of notable things in this code:
- The use of asynchronous methods for the communication process.
There are a couple of ways you can implement an HTTP communication in Qt. We chose to use QNetworkAccessManager. In order to use our code without problems you need to have Qt v4.7 or newer installed.
Known problems:
The HTTP methods (aka HTTP verbs) GET, POST, PUT, HEAD and DELETE work smoothly. Other HTTP methods will make use of QNetworkAccessManager::sendCustomRequest() which did not work properly in our tests. We were unable to find the source of this issue but we do not intend to give it any more time because other HTTP methods are rarely used. It feels as a Qt bug. For future reference this issue was found using Qt 5.2.1, Qt Creator 3.0.1, Qmake on a Mac OSX 10.9.4 (Maverics) . - The significance of specialized classes for the HTTP input.
This code uses the class HttpRequestInput as an encapsulation for input variables of the HTTP request. - The role of http_attribute_encode()
The original HTTP protocols are not very good at handling non-latin characters in attribute values. RFC 5987 makes the necessary provisions for a well thought out way to handle UTF-8 (unicode) characters. This function implements the rules set to handle non-latin characters in HTTP attribute values. - Trigger code
The code we just showed includes the function on_pushButton_clicked(), an event handler of a button click from the UI. The next sections of this article will differentiate this function to implement different types of HTTP requests. - Result handling
This code uses the handle_result() function to handle the asynchronous communication result. - Little things you need to adjust in your implementation:
This code sets the HTTP header for the name of the HTTP client/agent. You need to change "Agent name goes here" with the name of your client.
You obviously need to set the right code for triggering the code in on_pushButton_clicked() . Read the next sections for more examples on this function.
Naturally, you should customize handle_result() . Our code is just an example of the different things you can do when the communication is completed.
Simple HTTP GET requests
Lets see how to run a simple GET request.
void MainWindow::on_pushButton_clicked() { QString url_str = "http://www.example.com/path/to/page.php"; HttpRequestInput input(url_str, "GET"); HttpRequestWorker *worker = new HttpRequestWorker(this); connect(worker, SIGNAL(on_execution_finished(HttpRequestWorker*)), this, SLOT(handle_result(HttpRequestWorker*))); worker->execute(&input); }
This will produce the following HTTP request:
GET /path/to/page.php HTTP/1.1 User-Agent: Agent name goes here Connection: Keep-Alive Accept-Encoding: gzip, deflate Accept-Language: en-US,* Host: www.example.com
The previous example calls a plain URL. Lets add a few variables.
void MainWindow::on_pushButton_clicked() { QString url_str = "http://www.example.com/path/to/page.php"; HttpRequestInput input(url_str, "GET"); input.add_var("key1", "value1"); input.add_var("key2", "value2"); HttpRequestWorker *worker = new HttpRequestWorker(this); connect(worker, SIGNAL(on_execution_finished(HttpRequestWorker*)), this, SLOT(handle_result(HttpRequestWorker*))); worker->execute(&input); }
This will produce the following HTTP request:
GET /path/to/page.php?key1=value1&key2=value2 HTTP/1.1 User-Agent: Agent name goes here Connection: Keep-Alive Accept-Encoding: gzip, deflate Accept-Language: en-US,* Host: www.example.com
URL encoded HTTP POST requests
We can make a slight adjustment and turn the GET request to a URL encoded POST request.
void MainWindow::on_pushButton_clicked() { QString url_str = "http://www.example.com/path/to/page.php"; HttpRequestInput input(url_str, "POST"); input.add_var("key1", "value1"); input.add_var("key2", "value2"); HttpRequestWorker *worker = new HttpRequestWorker(this); connect(worker, SIGNAL(on_execution_finished(HttpRequestWorker*)), this, SLOT(handle_result(HttpRequestWorker*))); worker->execute(&input); }
This will produce the following HTTP request:
POST /path/to/page.php HTTP/1.1 User-Agent: Agent name goes here Content-Type: application/x-www-form-urlencoded Content-Length: 23 Connection: Keep-Alive Accept-Encoding: gzip, deflate Accept-Language: en-US,* Host: www.example.com key1=value1&key2=value2
Multipart HTTP POST requests
Finally, lets push it to the limits. Lets upload some files using a multipart POST request.
void MainWindow::on_pushButton_clicked() { QString url_str = "http://www.example.com/path/to/page.php"; HttpRequestInput input(url_str, "POST"); input.add_var("key1", "value1"); input.add_var("key2", "value2"); input.add_file("file1", "/path/to/file1.png", NULL, "image/png"); input.add_file("file2", "/path/to/file2.png", NULL, "image/png"); HttpRequestWorker *worker = new HttpRequestWorker(this); connect(worker, SIGNAL(on_execution_finished(HttpRequestWorker*)), this, SLOT(handle_result(HttpRequestWorker*))); worker->execute(&input); }
This will produce the following HTTP request:
POST /path/to/page.php HTTP/1.1 User-Agent: Agent name goes here Content-Type: multipart/form-data; boundary=__-----------------------9446182961397930864818 Content-Length: 686 Connection: Keep-Alive Accept-Encoding: gzip, deflate Accept-Language: en-US,* Host: www.example.com --__-----------------------9446182961397930864818 Content-Disposition: form-data; name="key1" Content-Type: text/plain value1 --__-----------------------9446182961397930864818 Content-Disposition: form-data; name="key2" Content-Type: text/plain value2 --__-----------------------9446182961397930864818 Content-Disposition: form-data; name="file1"; filename="file1.png" Content-Type: image/png Content-Transfer-Encoding: binary [... contents of /path/to/file1.png ...] --__-----------------------9446182961397930864818 Content-Disposition: form-data; name="file2"; filename="file2.png" Content-Type: image/png Content-Transfer-Encoding: binary [... contents of /path/to/file2.png ...] --__-----------------------9446182961397930864818--