Skip to content

Commit

Permalink
More robust API request/response handling (#70)
Browse files Browse the repository at this point in the history
* Add `User-Agent` header to all requests.

* Put all GET request parameters in URL as query params.

* Check response MIME type and throw an error if not JSON.
  • Loading branch information
krazkidd authored Jul 31, 2024
1 parent 7246a3a commit c340558
Show file tree
Hide file tree
Showing 9 changed files with 190 additions and 101 deletions.
2 changes: 2 additions & 0 deletions include/api/Api.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

#include <map>
#include <memory>
#include <string>
#include <string_view>
#include <list>
#include <variant>
Expand Down Expand Up @@ -60,6 +61,7 @@ namespace kdeck
std::list<std::shared_ptr<MarketPosition>> GetMarketPositions(std::string_view eventTicker = "");

private:
std::string userAgent;
std::string basePath;
std::shared_ptr<oatpp::parser::json::mapping::ObjectMapper> objectMapper;
std::shared_ptr<_Api> _api;
Expand Down
29 changes: 24 additions & 5 deletions include/api/Api.hxx
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,35 @@ namespace kdeck
case 404:
case 500:
case 503:
auto errorResponse = response->readBodyToDto<oatpp::Object<ErrorResponse>>(objectMapper.get());
oatpp::String contentType = response->getHeaders().get("Content-Type");

if (errorResponse->error->message)
// the MIME type could contain parameters (e.g. "application/json; charset=utf-8")
if (contentType->find("application/json") != std::string::npos)
{
OATPP_LOGE("Api", "Error => %s", errorResponse->error->message->c_str());
auto errorResponse = response->readBodyToDto<oatpp::Object<ErrorResponse>>(objectMapper.get());

if (errorResponse->error->message)
{
OATPP_LOGE("Api", "Error => %s", errorResponse->error->message->c_str());
}

return errorResponse.getPtr();
}
else
{
// NOTE: It appears the API may be behind a Web Application Firewall (WAF)
// and in some cases is rejecting requests with an HTML response. We
// consider this a hard error and throw, since it violates the spec.
// Source: https://github.com/krazkidd/kdeck/issues/69

return errorResponse.getPtr();
oatpp::String errorBody = response->readBodyToString();

OATPP_LOGE("Api", "Error => (%s)\n%s", contentType->c_str(), errorBody->c_str());

throw std::runtime_error("Unknown API error.");
}
}

throw std::runtime_error("Unknown JSON error.");
throw std::runtime_error("Unknown API error.");
}
}
161 changes: 138 additions & 23 deletions include/api/_Api.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#ifndef _API_HPP

Check notice on line 1 in include/api/_Api.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

Run clang-format on include/api/_Api.hpp

File include/api/_Api.hpp does not conform to LLVM style guidelines. (lines 9, 10, 11, 13, 14, 15, 17, 18, 20, 22, 23, 25, 26, 27, 28, 29, 30, 32, 33, 34, 35, 36, 37, 39, 40, 42, 43, 44, 45, 46, 48, 49, 50, 51, 52, 54, 55, 56, 57, 58, 60, 61, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 136, 137, 138, 139, 140, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 158, 159, 160, 161, 162, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 182, 183, 185, 186, 187, 188, 189, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 232, 233, 234, 235, 236, 237, 239, 240, 241, 242, 243, 244, 246, 247, 248, 249, 250, 251, 253, 254, 255, 256, 257, 258, 260, 261, 262, 263, 264, 265, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 303, 307)
#define _API_HPP

#include "oatpp/core/Types.hpp"

Check failure on line 4 in include/api/_Api.hpp

View workflow job for this annotation

GitHub Actions / cpp-linter

include/api/_Api.hpp:4:10 [clang-diagnostic-error]

'oatpp/core/Types.hpp' file not found
#include "oatpp/core/macro/codegen.hpp"
#include "oatpp/parser/json/mapping/ObjectMapper.hpp"
#include "oatpp/web/client/ApiClient.hpp"
Expand All @@ -26,14 +27,14 @@ namespace kdeck
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("POST", "{basePath}/login", Login, PATH(String, basePath), BODY_DTO(Object<LoginRequest>, req))
API_CALL("POST", "{basePath}/login", Login, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath), BODY_DTO(Object<LoginRequest>, req))

API_CALL_HEADERS(Logout)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("POST", "{basePath}/logout", Logout, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"))
API_CALL("POST", "{basePath}/logout", Logout, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"))

// exchange /////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
Expand All @@ -42,19 +43,19 @@ namespace kdeck
{
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/exchange/announcements", GetExchangeAnnouncements, PATH(String, basePath))
API_CALL("GET", "{basePath}/exchange/announcements", GetExchangeAnnouncements, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath))

API_CALL_HEADERS(GetExchangeSchedule)
{
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/exchange/schedule", GetExchangeSchedule, PATH(String, basePath))
API_CALL("GET", "{basePath}/exchange/schedule", GetExchangeSchedule, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath))

API_CALL_HEADERS(GetExchangeStatus)
{
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/exchange/status", GetExchangeStatus, PATH(String, basePath))
API_CALL("GET", "{basePath}/exchange/status", GetExchangeStatus, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath))

// market ///////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
Expand All @@ -64,54 +65,119 @@ namespace kdeck
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/events", GetEvents, PATH(String, basePath), BODY_DTO(Object<EventsRequest>, req))
API_CALL(
"GET",
"{basePath}/events",
GetEvents,
HEADER(String, userAgent, "User-Agent"),
PATH(String, basePath),
QUERY(String, cursor),
QUERY(Int32, limit),
QUERY(String, status),
QUERY(String, series_ticker),
QUERY(Boolean, with_nested_markets)
)

API_CALL_HEADERS(GetEvent)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/events/{event_ticker}", GetEvent, PATH(String, basePath), PATH(String, event_ticker), BODY_DTO(Object<EventRequest>, req))
API_CALL(
"GET",
"{basePath}/events/{event_ticker}",
GetEvent,
HEADER(String, userAgent, "User-Agent"),
PATH(String, basePath),
PATH(String, event_ticker),
QUERY(Boolean, with_nested_markets)
)

API_CALL_HEADERS(GetMarkets)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/markets", GetMarkets, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), BODY_DTO(Object<MarketsRequest>, req))
API_CALL(
"GET",
"{basePath}/markets",
GetMarkets,
HEADER(String, userAgent, "User-Agent"),
PATH(String, basePath),
AUTHORIZATION(String, authString, "Bearer"),
QUERY(String, cursor),
QUERY(Int32, limit),
QUERY(String, event_ticker),
QUERY(String, series_ticker),
QUERY(Int64, max_close_ts),
QUERY(Int64, min_close_ts),
QUERY(String, status),
QUERY(String, tickers)
)

API_CALL_HEADERS(GetTrades)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/markets/trades", GetTrades, PATH(String, basePath), BODY_DTO(Object<TradesRequest>, req))
API_CALL(
"GET",
"{basePath}/markets/trades",
GetTrades,
HEADER(String, userAgent, "User-Agent"),
PATH(String, basePath),
QUERY(String, cursor),
QUERY(Int32, limit),
QUERY(String, ticker),
QUERY(Int64, min_ts),
QUERY(Int64, max_ts)
)

API_CALL_HEADERS(GetMarket)
{
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/markets/{ticker}", GetMarket, PATH(String, basePath), PATH(String, ticker))
API_CALL("GET", "{basePath}/markets/{ticker}", GetMarket, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath), PATH(String, ticker))

API_CALL_HEADERS(GetMarketOrderbook)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/markets/{ticker}/orderbook", GetMarketOrderbook, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), QUERY(String, ticker), BODY_DTO(Object<MarketOrderbookRequest>, req))
API_CALL(
"GET",
"{basePath}/markets/{ticker}/orderbook",
GetMarketOrderbook,
HEADER(String, userAgent, "User-Agent"),
PATH(String, basePath),
AUTHORIZATION(String, authString, "Bearer"),
QUERY(String, ticker),
QUERY(Int32, depth)
)

API_CALL_HEADERS(GetSeries)
{
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/series/{series_ticker}", GetSeries, PATH(String, basePath), PATH(String, series_ticker))
API_CALL("GET", "{basePath}/series/{series_ticker}", GetSeries, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath), PATH(String, series_ticker))

API_CALL_HEADERS(GetMarketCandlesticks)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/series/{series_ticker}/markets/{ticker}/candlesticks", GetMarketCandlesticks, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), PATH(String, series_ticker), PATH(String, ticker), BODY_DTO(Object<MarketCandlesticksRequest>, req))
API_CALL(
"GET",
"{basePath}/series/{series_ticker}/markets/{ticker}/candlesticks",
GetMarketCandlesticks,
HEADER(String, userAgent, "User-Agent"),
PATH(String, basePath),
AUTHORIZATION(String, authString, "Bearer"),
PATH(String, series_ticker), PATH(String, ticker),
QUERY(Int64, start_ts),
QUERY(Int64, end_ts),
QUERY(Int64, period_interval)
)

// portfolio ////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////////
Expand All @@ -120,70 +186,119 @@ namespace kdeck
{
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/portfolio/balance", GetBalance, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"))
API_CALL("GET", "{basePath}/portfolio/balance", GetBalance, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"))

API_CALL_HEADERS(GetFills)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/portfolio/fills", GetFills, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), BODY_DTO(Object<FillsRequest>, req))
API_CALL(
"GET",
"{basePath}/portfolio/fills",
GetFills,
HEADER(String, userAgent, "User-Agent"),
PATH(String, basePath),
AUTHORIZATION(String, authString, "Bearer"),
QUERY(String, ticker),
QUERY(String, order_id),
QUERY(Int64, min_ts),
QUERY(Int64, max_ts),
QUERY(Int32, limit),
QUERY(String, cursor)
)

API_CALL_HEADERS(GetOrders)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/portfolio/orders", GetOrders, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), BODY_DTO(Object<OrdersRequest>, req))
API_CALL(
"GET",
"{basePath}/portfolio/orders",
GetOrders,
HEADER(String, userAgent, "User-Agent"),
PATH(String, basePath),
AUTHORIZATION(String, authString, "Bearer"),
QUERY(String, ticker),
QUERY(String, event_ticker),
QUERY(Int64, min_ts),
QUERY(Int64, max_ts),
QUERY(String, status),
QUERY(String, cursor),
QUERY(Int32, limit)
)

API_CALL_HEADERS(CreateOrder)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("POST", "{basePath}/portfolio/orders", CreateOrder, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), BODY_DTO(Object<CreateOrderRequest>, req))
API_CALL("POST", "{basePath}/portfolio/orders", CreateOrder, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), BODY_DTO(Object<CreateOrderRequest>, req))

API_CALL_HEADERS(GetOrder)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/portfolio/orders/{order_id}", GetOrder, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), PATH(String, order_id))
API_CALL("GET", "{basePath}/portfolio/orders/{order_id}", GetOrder, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), PATH(String, order_id))

API_CALL_HEADERS(CancelOrder)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("DELETE", "{basePath}/portfolio/orders/{order_id}", CancelOrder, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), PATH(String, order_id))
API_CALL("DELETE", "{basePath}/portfolio/orders/{order_id}", CancelOrder, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), PATH(String, order_id))

API_CALL_HEADERS(AmendOrder)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("POST", "{basePath}/portfolio/orders/{order_id}/amend", AmendOrder, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), PATH(String, order_id), BODY_DTO(Object<AmendOrderRequest>, req))
API_CALL("POST", "{basePath}/portfolio/orders/{order_id}/amend", AmendOrder, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), PATH(String, order_id), BODY_DTO(Object<AmendOrderRequest>, req))

API_CALL_HEADERS(DecreaseOrder)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("POST", "{basePath}/portfolio/orders/{order_id}/decrease", DecreaseOrder, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), PATH(String, order_id), BODY_DTO(Object<DecreaseOrderRequest>, req))
API_CALL("POST", "{basePath}/portfolio/orders/{order_id}/decrease", DecreaseOrder, HEADER(String, userAgent, "User-Agent"), PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), PATH(String, order_id), BODY_DTO(Object<DecreaseOrderRequest>, req))

API_CALL_HEADERS(GetPositions)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/portfolio/positions", GetPositions, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), BODY_DTO(Object<PositionsRequest>, req))
API_CALL(
"GET",
"{basePath}/portfolio/positions",
GetPositions,
HEADER(String, userAgent, "User-Agent"),
PATH(String, basePath),
AUTHORIZATION(String, authString, "Bearer"),
QUERY(String, cursor),
QUERY(Int32, limit),
QUERY(String, count_filter),
QUERY(String, settlement_status),
QUERY(String, ticker),
QUERY(String, event_ticker)
)

API_CALL_HEADERS(GetPortfolioSettlements)
{
headers.put("Content-Type", "application/json");
headers.put("Accept", "application/json");
}
API_CALL("GET", "{basePath}/portfolio/settlements", GetPortfolioSettlements, PATH(String, basePath), AUTHORIZATION(String, authString, "Bearer"), BODY_DTO(Object<PortfolioSettlementsRequest>, req))
API_CALL(
"GET",
"{basePath}/portfolio/settlements",
GetPortfolioSettlements,
HEADER(String, userAgent, "User-Agent"),
PATH(String, basePath),
AUTHORIZATION(String, authString, "Bearer"),
QUERY(Int64, limit),
QUERY(String, cursor)
)

};

Expand Down
Loading

0 comments on commit c340558

Please sign in to comment.