Improve error handling on rest api
[ric-app/ts.git] / src / utils / restclient.cpp
1 // vi: ts=4 sw=4 noet:
2 /*
3 ==================================================================================
4     Copyright (c) 2022 Alexandre Huff
5
6    Licensed under the Apache License, Version 2.0 (the "License");
7    you may not use this file except in compliance with the License.
8    You may obtain a copy of the License at
9
10        http://www.apache.org/licenses/LICENSE-2.0
11
12    Unless required by applicable law or agreed to in writing, software
13    distributed under the License is distributed on an "AS IS" BASIS,
14    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15    See the License for the specific language governing permissions and
16    limitations under the License.
17 ==================================================================================
18 */
19
20 /*
21     Mnemonic:   restclient.cpp
22     Abstract:   Implements a tiny wrapper on top of libcurl to handle with rest requests.
23
24     Date:       8 Apr 2022
25     Author:     Alexandre Huff
26 */
27
28
29 #include "restclient.hpp"
30 #include <mutex>
31 #include <string.h>
32 #include <memory>
33 #include <sstream>
34
35 namespace restclient {
36
37 // callback to handle http responses
38 static size_t http_response_callback( const char *in, size_t size, size_t num, std::string *out ) {
39     if( out == NULL ) {
40         return 0;
41     }
42     const size_t totalBytes( size * num );
43     out->append( in, totalBytes );
44     return totalBytes;
45 }
46
47 /*
48     Create a RestClient instance to exchange messages with
49     a given rest api available on baseUrl, which consists of
50     scheme://domain[:port]
51 */
52 RestClient::RestClient( std::string baseUrl ) {
53     this->baseUrl = baseUrl;
54
55     init();
56 }
57
58 RestClient::~RestClient( ) {
59     curl_slist_free_all( headers );
60     curl_easy_cleanup( curl );
61 }
62
63 std::string RestClient::getBaseUrl( ) {
64     return baseUrl;
65 }
66
67 void RestClient::init( ) {
68     static std::mutex curl_mutex;
69
70     {   // scoped mutex to make curl_global_init thread-safe
71         const std::lock_guard<std::mutex> lock( curl_mutex );
72         CURLcode code = curl_global_init( CURL_GLOBAL_DEFAULT );
73         if( code != 0 ) {
74             std::stringstream ss;
75             ss << "curl_global_init returned error code " << code << ", unable to proceed";
76             std::string s = std::to_string(code);
77             throw RestClientException( ss.str() );
78         }
79     }
80
81     try {
82         curl = curl_easy_init();
83         if( curl == NULL ) {
84             throw RestClientException( "CURL did not return a handler, unable to proceed" );
85         }
86
87         // curl_easy_setopt( curl, CURLOPT_VERBOSE, 1L );
88         if( curl_easy_setopt( curl, CURLOPT_TIMEOUT, 5 ) != CURLE_OK ) {
89             throw RestClientException( "unable to set CURLOPT_TIMEOUT" );
90         }
91
92         /* provide a buffer to store errors in */
93         if( curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, errbuf) != CURLE_OK ) {
94             throw RestClientException( "unable to set CURLOPT_ERRORBUFFER" );
95         }
96         errbuf[0] = 0;
97
98         if( curl_easy_setopt( curl, CURLOPT_WRITEFUNCTION, http_response_callback ) != CURLE_OK ) {
99             throw RestClientException( "unable to set CURLOPT_WRITEFUNCTION" );
100         }
101
102         headers = curl_slist_append( headers, "Accept: application/json" );
103         headers = curl_slist_append( headers, "Content-Type: application/json" );
104         if( curl_easy_setopt( curl, CURLOPT_HTTPHEADER, headers ) != CURLE_OK ) {
105             throw RestClientException( "unable to set CURLOPT_HTTPHEADER" );
106         }
107
108     } catch( const RestClientException &e ) {   // avoid memory leakage
109         if( headers != NULL ) {
110             curl_slist_free_all( headers );
111         }
112         if( curl != NULL ) {
113             curl_easy_cleanup( curl );
114         }
115
116         std::stringstream ss;
117         ss << "Failed to initialize RestClient: " << e.what();
118         throw RestClientException( ss.str() );
119     }
120
121 }
122
123 /*
124     Executes a GET request at the path of this RestClient instance.
125     Returns the HTTP status code and the correspoding message body.
126 */
127 response_t RestClient::do_get( std::string path ) {
128     response_t response = { 0, "" };
129
130     const std::string endpoint = baseUrl + path;
131
132     if( curl_easy_setopt( curl, CURLOPT_WRITEDATA, &response.body ) != CURLE_OK ) {
133         throw RestClientException( "unable to set CURLOPT_WRITEDATA" );
134     }
135
136     if( curl_easy_setopt( curl, CURLOPT_URL, endpoint.c_str() ) != CURLE_OK ) {
137         throw RestClientException( "unable to set CURLOPT_URL" );
138     }
139     if( curl_easy_setopt( curl, CURLOPT_HTTPGET, 1L ) != CURLE_OK ) {
140         throw RestClientException( "unable to set CURLOPT_HTTPGET" );
141     }
142
143     CURLcode res = curl_easy_perform( curl );
144     if( res == CURLE_OK ) {
145         if( curl_easy_getinfo( curl, CURLINFO_RESPONSE_CODE, &response.status_code ) != CURLE_OK ) {
146             throw RestClientException( std::string("unable to get CURLINFO_RESPONSE_CODE. ") + errbuf );
147         }
148     } else {
149         size_t len = strlen( errbuf );
150         std::stringstream ss;
151         ss << "unable to complete the request at " << endpoint.c_str();
152         if(len)
153             ss << ". " << errbuf;
154         else
155             ss << ". " << curl_easy_strerror( res );
156
157         throw RestClientException( ss.str() );
158     }
159
160     return response;
161 }
162
163 /*
164     Executes a POST request of a json message at the path of this RestClient instance.
165     Returns the HTTP status code and the correspoding message body.
166 */
167 response_t RestClient::do_post( std::string path, std::string json ) {
168     response_t response = { 0, "" };
169
170     const std::string endpoint = baseUrl + path;
171
172     if( curl_easy_setopt( curl, CURLOPT_WRITEDATA, &response.body ) != CURLE_OK ) {
173         throw RestClientException( "unable to set CURLOPT_WRITEDATA" );
174     }
175
176     if( curl_easy_setopt( curl, CURLOPT_URL, endpoint.c_str() ) != CURLE_OK ) {
177         throw RestClientException( "unable to set CURLOPT_URL" );
178     }
179     if( curl_easy_setopt( curl, CURLOPT_POST, 1L ) != CURLE_OK ) {
180         throw RestClientException( "unable to set CURLOPT_POST" );
181     }
182     if( curl_easy_setopt( curl, CURLOPT_POSTFIELDS, json.c_str() ) != CURLE_OK ) {
183         throw RestClientException( "unable to set CURLOPT_POSTFIELDS" );
184     }
185
186     CURLcode res = curl_easy_perform( curl );
187     if( res == CURLE_OK ) {
188         if( curl_easy_getinfo( curl, CURLINFO_RESPONSE_CODE, &response.status_code ) != CURLE_OK ) {
189             throw RestClientException( std::string("unable to get CURLINFO_RESPONSE_CODE. ") + errbuf );
190         }
191     } else {
192         size_t len = strlen( errbuf );
193         std::stringstream ss;
194         ss << "unable to complete the request at " << endpoint.c_str();
195         if(len)
196             ss << ". " << errbuf;
197         else
198             ss << ". " << curl_easy_strerror( res );
199
200         throw RestClientException( ss.str() );
201     }
202
203     return response;
204 }
205
206 } // namespace