C++ Web Services
Routing
flowchart TD 200[200 OK] 404[404 Not Found] A[HTTP Request] B[psinode] C[http-server service] D[sites service's serveSys action] A --> B --> C --> D --> E{{was site data found?}} -->|yes| 200 E -->|no| G{{target begins with '/common'?}} -->|yes| H['common-api' service's serveSys action] G -->|no| I{{on a subdomain?}} -->|no| 404 I -->|yes| J{{Has registered server?}} -->|no| 404 J -->|yes| L[registered server's serveSys action]
psinode
passes most HTTP requests to the SystemService::HttpServer service, which then routes requests to the appropriate service's serveSys action (see diagram). The services run in RPC mode; this prevents them from writing to the database, but allows them to read data they normally can't. See psibase::DbId.
SystemService::CommonApi provides services common to all domains under the /common
tree. It also serves the chain's main page.
SystemService::Sites provides web hosting for non-service accounts or service accounts that did not register for HTTP handling.
psinode
directly handles requests which start with /native
, e.g. /native/push_boot
. Services don't serve these.
Registration
Services which wish to serve HTTP requests need to register using the SystemService::HttpServer service's SystemService::HttpServer::registerServer action. There are multiple ways to do this:
psibase deploy
has a--register-proxy
option (shortcut-p
) that can do this while deploying the service.psibase register-proxy
can also do it. TODO: implementpsibase register-proxy
.- A service may call
registerServer
during its own initialization action.
A service doesn't have to serve HTTP requests itself; it may delegate this to another service during registration.
HTTP Interfaces
Services which serve HTTP implements this interface:
psibase::ServerInterface
struct psibase::ServerInterface {
serveSys(...); // Handle HTTP requests
psio_get_reflect_impl(...);
};
Interface for services which serve http.
http-server
uses this interface to call into services to respond to http requests.
⚠️ Do not inherit from this. To implement this interface, add a serveSys action to your service and reflect it.
psibase::ServerInterface::serveSys
std::optional<HttpReply> psibase::ServerInterface::serveSys(
HttpRequest request,
std::optional<std::int32_t> socket
);
Handle HTTP requests.
Define this action in your service to handle HTTP requests. You'll also need to register your service with http-server.
serveSys
can do any of the following:
- Return
std::nullopt
to signal not found. psinode produces a 404 response in this case. - Abort. psinode produces a 500 response with the service's abort message.
- Return a psibase::HttpReply. psinode produces a 200 response with the body and contentType returned.
- Call other services.
- Call
http-server::sendReply
. Explicitly sends a response. - Call
http-server::deferReply
. No response will be produced untilhttp-server::sendReply
is called.
A service runs in RPC mode while serving an HTTP request. This mode prevents database writes, but allows database reads, including reading data and events which are normally not available to services; see psibase::DbId.
psibase::ServerInterface::psio_get_reflect_impl
psio_reflect_impl_ServerInterface<ServerInterface> psibase::ServerInterface::psio_get_reflect_impl(
ServerInterface * ,
::psio::ReflectDummyParam *
);
psibase::HttpRequest
struct psibase::HttpRequest {
std::string host; // Fully-qualified domain name
std::string rootHost; // host, but without service subdomain
std::string method; // "GET", "POST", "OPTIONS", "HEAD"
std::string target; // Absolute path, e.g. "/index.js"
std::string contentType; // "application/json", "text/html", ...
std::vector<HttpHeader> headers; // HTTP Headers
std::vector<char> body; // Request body, e.g. POST data
psio_get_reflect_impl(...);
readQueryItem(...);
query(...); // Parses the query component
path(...); // Returns the path component
};
An HTTP Request.
Most services receive this via their serveSys
action.
SystemService::HttpServer receives it via its serve
exported function.
psibase::HttpRequest::psio_get_reflect_impl
psio_reflect_impl_HttpRequest<HttpRequest> psibase::HttpRequest::psio_get_reflect_impl(
HttpRequest * ,
::psio::ReflectDummyParam *
);
psibase::HttpRequest::readQueryItem
std::pair<std::string, std::string> psibase::HttpRequest::readQueryItem(
std::string_view &
);
psibase::HttpRequest::query
template<typename T>
T psibase::HttpRequest::query() const;
Parses the query component.
T must be a reflected type, whose fields are assignable from
a temporary string. The names of the fields are the query keys.
The query should have the form key1=value1&key2=value2...
The values will have %XX escapes decoded.
psibase::HttpRequest::path
std::string psibase::HttpRequest::path() const;
Returns the path component.
psibase::HttpReply
struct psibase::HttpReply {
HttpStatus status;
std::string contentType; // "application/json", "text/html", ...
std::vector<char> body; // Response body
std::vector<HttpHeader> headers; // HTTP Headers
psio_get_reflect_impl(...);
};
An HTTP reply.
Services return this from their serveSys
action.
psibase::HttpReply::psio_get_reflect_impl
psio_reflect_impl_HttpReply<HttpReply> psibase::HttpReply::psio_get_reflect_impl(
HttpReply * ,
::psio::ReflectDummyParam *
);
Helpers
These help implement basic functionality:
- psibase::serveSimpleUI
- psibase::serveActionTemplates
- psibase::servePackAction
- psibase::serveSchema
- psibase::serveGraphQL
- psibase::makeConnection
- psibase::EventDecoder
- psibase::EventQuery
Here's a common pattern for using these functions:
std::optional<psibase::HttpReply> serveSys(psibase::HttpRequest request)
{
if (auto result = psibase::serveActionTemplates<ExampleService>(request))
return result;
if (auto result = psibase::servePackAction<ExampleService>(request))
return result;
if (request.method == "GET" && request.target == "/")
{
static const char helloWorld[] = "Hello World";
return psibase::HttpReply{
.contentType = "text/plain",
.body = {helloWorld, helloWorld + strlen(helloWorld)},
};
}
return std::nullopt;
}
psibase::serveSimpleUI
template<typename Service, bool IncludeRoot>
std::optional<HttpReply> psibase::serveSimpleUI(
const HttpRequest & request
);
Serve a developer UI.
This function serves a simple developer UI to help get you started. The UI it generates is not suitable for end users.
This serves the following:
GET /action_templates
: provided by serveActionTemplatesPOST /pack_action/x
: provided by servePackActionGET /
, but only ifIncludeRoot
is set. This returns the following HTML body:
<html>
<div id="root" class="ui container"></div>
<script src="/common/SimpleUI.mjs" type="module"></script>
</html>
psibase::serveActionTemplates
template<typename Service>
std::optional<HttpReply> psibase::serveActionTemplates(
const HttpRequest & request
);
Handle /action_templates
request.
If request
is a GET to /action_templates
, then this returns a
JSON object containing a field for each action in Service
. The
field names match the action names. The field values are objects
with the action arguments, each containing sample data.
If request
doesn't match the above, then this returns std::nullopt
.
psibase::servePackAction
template<typename Service>
std::optional<HttpReply> psibase::servePackAction(
const HttpRequest & request
);
Handle /pack_action/
request.
If request
is a POST to /pack_action/x
, where x
is an action
on Service
, then this parses a JSON object containing the arguments
to x
, packs them using fracpack, and returns the result as an
application/octet-stream
.
If request
doesn't match the above, or the action name is not found,
then this returns std::nullopt
.
psibase::serveSchema
template<typename Service>
std::optional<HttpReply> psibase::serveSchema(
const HttpRequest & request
);
Handle /schema
request.
If request
is a GET to /schema
, then this returns JSON for a
ServiceSchema object.
If request
doesn't match the above, then this returns std::nullopt
.
psibase::serveGraphQL
template<typename QueryRoot>
std::optional<HttpReply> psibase::serveGraphQL(
const HttpRequest & request,
const QueryRoot & queryRoot
);
Handle /graphql
request.
This handles graphql requests, including fetching the schema.
GET /graphql
: Returns the schema.GET /graphql?query=...
: Run query in URL and return JSON result.POST /graphql?query=...
: Run query in URL and return JSON result.POST /graphql
withContent-Type = application/graphql
: Run query that's in body and return JSON result.POST /graphql
withContent-Type = application/json
: Body contains a JSON object of the form{"query"="..."}
. Run query and return JSON result.
queryRoot
should be a reflected object; this shows up in GraphQL as the root
Query
type. GraphQL exposes both fields and const methods. Fields may be
any reflected struct. Const methods may return any reflected struct. They should
return objects by value.
psibase::makeConnection
template<typename Connection, typename T, typename Key, typename Proj = std::identity>
Connection psibase::makeConnection(
const TableIndex<T, Key> & index,
const std::optional<Key> & gt,
const std::optional<Key> & ge,
const std::optional<Key> & lt,
const std::optional<Key> & le,
std::optional<uint32_t> first,
std::optional<uint32_t> last,
const std::optional<std::string> & before,
const std::optional<std::string> & after,
Proj && proj = {}
);
GraphQL Pagination through TableIndex.
You rarely need to call this directly; see the example.
Template arguments:
Connection
: ConnectionT
: Type stored in indexKey
: Key in index
Arguments:
index
: TableIndex to paginate throughgt
: Restrict range to keys greater than thisge
: Restrict range to keys greater than or equal to thislt
: Restrict range to keys less than to thisle
: Restrict range to keys less than or equal to thisfirst
: Stop after including this many items at the beginning of the rangelast
: Stop after including this many items at the end of the rangebefore
: Opaque cursor value. Resume paging; include keys before this pointafter
: Opaque cursor value. Resume paging; include keys after this point
By default, makeConnection
pages through the entire index's range.
gt
, ge
, lt
, and le
restrict the range. They can be used in any
combination (set intersection).
first
, last
, before
, after
, PageInfo, Connection, and
Edge match the GraphQL Cursor Connections Specification
(aka GraphQL Pagination). first
and after
page through the
range defined above in the forward direction. last
and before
page through the range defined above in the reverse direction.
Combining first
with last
isn't recommended, but matches the
behavior in the specification.
makeConnection example
This demonstrates exposing a table's contents via GraphQL. This example doesn't include a way to fill the table; that's left as an exercise to the reader. Hint: service-based RPC and GraphQL only support read-only operations; you must use actions to write to a table.
#include <psibase/Service.hpp>
#include <psibase/dispatch.hpp>
#include <psibase/serveGraphQL.hpp>
#include <psibase/serveSimpleUI.hpp>
struct MyType
{
uint32_t primaryKey;
std::string secondaryKey;
std::string moreData;
std::string someFn(std::string arg1, std::string arg2) const
{
return arg1 + secondaryKey + arg2 + moreData;
}
};
PSIO_REFLECT(MyType,
primaryKey, secondaryKey, moreData,
method(someFn, arg1, arg2))
using MyTable = psibase::Table<
MyType, &MyType::primaryKey, &MyType::secondaryKey>;
using MyTables = psibase::ServiceTables<MyTable>;
struct Query
{
psibase::AccountNumber service;
auto rowsByPrimary() const {
return MyTables{service}.open<MyTable>().getIndex<0>();
}
auto rowsBySecondary() const {
return MyTables{service}.open<MyTable>().getIndex<1>();
}
};
PSIO_REFLECT(Query, method(rowsByPrimary), method(rowsBySecondary))
struct ExampleService : psibase::Service<ExampleService>
{
std::optional<psibase::HttpReply> serveSys(psibase::HttpRequest request)
{
if (auto result = psibase::serveSimpleUI<ExampleService, true>(request))
return result;
if (auto result = psibase::serveGraphQL(request, Query{getReceiver()}))
return result;
return std::nullopt;
}
};
PSIO_REFLECT(ExampleService, method(serveSys, request))
PSIBASE_DISPATCH(ExampleService)
This example doesn't call makeConnection
directly; it's automatic.
If a member function on a query object:
- is
const
, and - is reflected, and
- has no arguments, and
- returns a TableIndex
then the system exposes that function to GraphQL as a function which
takes gt
, ge
, lt
, le
, first
, last
, before
, and after
.
The system calls makeConnection
automatically.
serveGraphQL generates this GraphQL schema and processes queries which conform to it:
type MyType {
primaryKey: Float!
secondaryKey: String!
moreData: String!
someFn(arg1: String! arg2: String!): String!
}
type PageInfo {
hasPreviousPage: Boolean!
hasNextPage: Boolean!
startCursor: String!
endCursor: String!
}
type MyTypeEdge {
node: MyType!
cursor: String!
}
type MyTypeConnection {
edges: [MyTypeEdge!]!
pageInfo: PageInfo!
}
type Query {
rowsByPrimary(
gt: Float ge: Float lt: Float le: Float
first: Float last: Float
before: String after: String): MyTypeConnection!
rowsBySecondary(
gt: String ge: String lt: String le: String
first: Float last: Float
before: String after: String): MyTypeConnection!
}
Things of note:
rowsByPrimary
androwsBySecondary
automatically havemakeConnection
's arguments.MyTypeEdge
andMyTypeConnection
are automatically generated fromMyType
.- Returned rows (
MyType
) include MyType's fields and thesomeFn
method. Onlyconst
methods are exposed. - serveGraphQL automatically chooses GraphQL types which cover the range of numeric types. When no suitable match is found (e.g. no GraphQL type covers the range of
int64_t
), it falls back toString
.
psibase::PageInfo
struct psibase::PageInfo {
bool hasPreviousPage;
bool hasNextPage;
std::string startCursor;
std::string endCursor;
psio_get_reflect_impl(...);
};
GraphQL support for paging.
This lets the query clients know when more data is available and what cursor values can be used to fetch that data.
psibase::PageInfo::psio_get_reflect_impl
psio_reflect_impl_PageInfo<PageInfo> psibase::PageInfo::psio_get_reflect_impl(
PageInfo * ,
::psio::ReflectDummyParam *
);
psibase::Edge
template<typename Node, psio::FixedString EdgeName>
struct psibase::Edge {
Node node;
std::string cursor;
psio_get_reflect_impl(...);
};
GraphQL support for paging.
node
contains the row data. cursor
identifies where in the
table the node is located.
psibase::Edge::psio_get_reflect_impl
psio_reflect_impl_Edge<Edge<Node, EdgeName>> psibase::Edge::psio_get_reflect_impl(
Edge<Node, EdgeName> * ,
::psio::ReflectDummyParam *
);
psibase::Connection
template<typename Node, psio::FixedString ConnectionName, psio::FixedString EdgeName>
struct psibase::Connection {
std::vector<Edge> edges;
PageInfo pageInfo;
psio_get_reflect_impl(...);
};
GraphQL support for paging.
edges
contain the matching rows. pageInfo
gives clients information
needed to resume paging.
psibase::Connection::psio_get_reflect_impl
psio_reflect_impl_Connection<Connection<Node, ConnectionName, EdgeName>> psibase::Connection::psio_get_reflect_impl(
Connection<Node, ConnectionName, EdgeName> * ,
::psio::ReflectDummyParam *
);
psibase::EventDecoder
template<typename Events>
struct psibase::EventDecoder {
DbId db;
uint64_t eventId;
AccountNumber service;
};
GraphQL support for decoding an event.
If a GraphQL query function returns this type, then the system fetches and decodes an event.
The GraphQL result is an object with these fields, plus more:
type MyService_EventsUi {
event_db: Float! # Database ID (uint32_t)
event_id: String! # Event ID (uint64_t)
event_found: Boolean! # Was the event found in db?
event_service: String! # Service that created the event
event_supported_service: Boolean! # Is this service the one
# that created it?
event_type: String! # Event type
event_unpack_ok: Boolean! # Did it decode OK?
}
EventDecoder
will only attempt to decode an event which meets all of the following:
- It's found in the
EventDecoder::db
database (event_found
will be true) - Was written by the service which matches the
EventDecoder::service
field (event_supported_service
will be true) - Has a type which matches one of the definitions in the
Events
template argument
If decoding is successful, EventDecoder
will set the GraphQL event_unpack_ok
field to true. It will include any event fields which were in the query request.
It will include all event fields if the query request includes the special field
event_all_content
. EventDecoder
silently ignores any requested fields which
don't match the fields of the decoded event.
EventDecoder example
This example assumes you're already serving GraphQL and have defined events for your service. It's rare to define a query method like this one; use EventQuery instead, which handles history, ui, and merkle events.
struct Query
{
psibase::AccountNumber service;
auto getUiEvent(uint64_t eventId) const
{
return EventDecoder<MyService::Events::Ui>{
DbId::uiEvent, eventId, service};
}
};
PSIO_REFLECT(Query, method(getUiEvent, eventId))
Example query:
{
getUiEvent(eventId: "13") {
event_id
event_type
event_all_content
}
}
Example reply:
{
"data": {
"getUiEvent": {
"event_id": "13",
"event_type": "credited",
"tokenId": 1,
"sender": "symbol",
"receiver": "alice",
"amount": {
"value": "100000000000"
},
"memo": {
"contents": "memo"
}
}
}
}
psibase::EventQuery
template<EventType Events>
struct psibase::EventQuery {
AccountNumber service;
history(...); // Decode history events
ui(...); // Decode user interface events
merkle(...); // Decode merkle events
psio_get_reflect_impl(...);
};
GraphQL support for decoding multiple events.
If a GraphQL query function returns this type, then the system returns a GraphQL object with the following query methods:
type MyService_Events {
history(ids: [String!]!): [MyService_EventsHistory!]!
ui(ids: [String!]!): [MyService_EventsUi!]!
merkle(ids: [String!]!): [MyService_EventsMerkle!]!
}
These methods take an array of event IDs. They return arrays
of objects containing the decoded (if possible) events.
See EventDecoder for how to interact with the return values;
MyService_EventsHistory
, MyService_EventsUi
, and
MyService_EventsMerkle
all behave the same.
EventQuery example
This example assumes you're already serving GraphQL and have defined events for your service.
struct Query
{
psibase::AccountNumber service;
auto events() const
{
return psibase::EventQuery<MyService::Events>{service};
}
};
PSIO_REFLECT(Query, method(events))
Example query:
{
events {
history(ids: ["3", "4"]) {
event_id
event_all_content
}
}
}
Example reply:
{
"data": {
"events": {
"history": [
{
"event_id": "3",
"tokenId": 1,
"creator": "tokens",
"precision": {
"value": 8
},
"maxSupply": {
"value": "100000000000000000"
}
},
{
"event_id": "4",
"prevEvent": 1,
"tokenId": "3",
"setter": "tokens",
"flag": "untradeable",
"enable": true
}
]
}
}
}
psibase::EventQuery::history
auto psibase::EventQuery::history(
const std::vector<uint64_t> & ids
) const;
Decode history events.
psibase::EventQuery::ui
auto psibase::EventQuery::ui(
const std::vector<uint64_t> & ids
) const;
Decode user interface events.
psibase::EventQuery::merkle
auto psibase::EventQuery::merkle(
const std::vector<uint64_t> & ids
) const;
Decode merkle events.
psibase::EventQuery::psio_get_reflect_impl
psio_reflect_impl_EventQuery<EventQuery<Events>> psibase::EventQuery::psio_get_reflect_impl(
EventQuery<Events> * ,
::psio::ReflectDummyParam *
);