Qore RestHandler Module Reference 1.6.2
Loading...
Searching...
No Matches
RestHandler Module

Introduction to the RestHandler Module

The RestHandler module provides functionality for implementing REST services with the Qore HTTP server.

To use this module, use "%requires RestHandler" and "%requires HttpServer" in your code.

All the public symbols in the module are defined in the RestHandler namespace.

The main classes are:

  • AbstractRestClass: this class provides the functionality of the REST object currently being accessed (and access to subobjects or subclasses)
  • RestHandler: this class can be used to plug in to the Qore HttpServer to expose REST services

see example file "restserver.q" in the examples directory for an example of using this module to implement REST services.

Data Serialization Support

The RestHandler class uses any of the following modules if the modules are available when the RestHandler module is initialized:

  • json: provides automatic support for JSON payload de/serialization
  • xml: provides automatic support for XML-RPC payload de/serialization
  • yaml: provides automatic support for YAML payload de/serialization

URL form encoding is supported as well for message bodies according to RFC 2738 2.2 with "Content-Type: application/x-www-form-urlencoded".

For standard REST web service development, the json module will be required to support JSON serialization.

The RestHandler class will automatically deserialize request bodies if the incoming MIME type is supported and additionally will serialize outgoing messages automatically based on the client's declared capabilities (normally responses are serialized with the same serialization as the incoming message).

Implementing REST Services

The class that models a REST entity or object in this module is AbstractRestClass.

This class should be subclassed for each object type, and methods implemented for each REST method that the object will support.

Incoming requests matched by your RestHandler subclass (when called by the HttpServer when requests are appropriately matched) will be dispatched to methods in your AbstractRestClass specialization (ie your user-defined subclass of this class) first according to the following naming convention:

  • httpmethod[RequestRestAction]

For example:

  • "GET /obj HTTP/1.1": matches method AbstractRestClass::get()
  • "POST /obj HTTP/1.1": matches method AbstractRestClass::post()
  • "DELETE /obj/subobj": match method AbstractRestClass::del()
  • "GET /obj?action=status HTTP/1.1": matches method AbstractRestClass::getStatus()
  • "PUT /obj?action=status HTTP/1.1": matches method AbstractRestClass::putStatus()
  • "PATCH /obj?action=status HTTP/1.1": matches method AbstractRestClass::patchStatus()

In other words, if a REST action is given in the request (either as a URI parameter or in the message body), the first letter of the action name is capitalized and appended to a lower case version of the HTTP method name (except "DELETE" is mapped to "del" because "delete" is a keyword); if such a method exists in your AbstractRestClass specialization, then it is called. If not, then, if the REST action exists under a different HTTP method (ie a request comes with "HTTP GET /something?action=setData", and "putSetData" exists, but "getSetData" was used for the search), then a 405 "Method Not Allowed" response is returned. If no variation of the requested REST method exists, then a 501 "Not Implemented" response is returned.

If a REST request is made with no action name, then a method in your class is attempted to be called with just the HTTP method name in lower case (except "DELETE" is mapped to "del" because "delete" is a keyword).

HTTP Method Name to Qore Method Name

HTTP Method Qore Method
GET AbstractRestClass::get()
PUT AbstractRestClass::put()
PATCH AbstractRestClass::patch()
POST AbstractRestClass::post()
DELETE AbstractRestClass::del()
OPTIONS AbstractRestClass::options()

If no action is provided in the URL and the final URI path component does not match to a subclass, and an action method is defined in the last REST class, then this method will be run.

For example (assuming no subclass match for the trailing "status" path component):

  • "GET /obj/status HTTP/1.1": matches method AbstractRestClass::getStatus()
  • "PUT /obj/status HTTP/1.1": matches method AbstractRestClass::putStatus()
  • "PATCH /obj/status HTTP/1.1": matches method AbstractRestClass::patchStatus()

It is recommended to avoid requiringg an "action" parameter, because this approach is generally not compatible with REST schema solutions such as Swaggger/OAS or RAML.

In all cases, the signature for these methods looks like:

hash<auto> method(hash<auto> cx, *hash<auto> ah) {}

The arguments passed to the REST action methods are as follows:

  • cx: a call context hash; the same value as documented in the cx parameter of HttpServer::AbstractHttpRequestHandler::handleRequest(); with the following additions:
    • body: the deserialized message body (if any); if no body was sent, then any parsed URI arguments are copied here
    • hdr: a copy of the header hash as returned by Socket::readHTTPHeader() as received by the HttpServer (for example cx.hdr.path gives the original URI request path)
    • orig_method: the string if any "action" argumnt was sent either in the message body or as a URI argument
    • rest_method: same as orig_method, only with the first letter capitalized (only present if an "action" argument was sent)
    • rest_action_method: the method name that will be called on the actual Qore REST class (a subclass of AbstractRestClass)
  • ah: these are the parsed URI query arguments to the REST call; see Argument and Message Body Handling for more information

The return value for these methods is the same as the return value for HttpServer::AbstractHttpRequestHandler::handleRequest().

consider the following example class:

class AbstractExampleInstanceBaseClass inherits AbstractRestClass {
private {
hash<auto> h;
}
constructor(hash<auto> n_h) {
h = n_h;
}
hash<auto> get(hash<auto> cx, *hash<auto> ah) {
return RestHandler::makeResponse(200, h);
}
hash<auto> putChown(hash<auto> cx, *hash<auto> ah) {
if (!exists cx.body.user && !exists cx.body.group)
return RestHandler::make400("missing 'user' and/or 'group' parameters for chown(%y)", h.path);
int rc = chown(h.path, cx.body.user, cx.body.group);
return rc ? RestHandler::make400("ERROR chown(%y): %s", h.path, strerror()) : RestHandler::makeResponse(200, "OK");
}
hash<auto> putChmod(hash<auto> cx, *hash<auto> ah) {
if (!exists cx.body.mode)
return RestHandler::make400("missing 'mode' parameter for chmod(%y)", h.path);
int rc = chmod(h.path, cx.body.mode);
return rc ? RestHandler::make400("ERROR chmod(%y): %s", h.path, strerror()) : RestHandler::makeResponse(200, "OK");
}
hash<auto> putRename(hash<auto> cx, *hash<auto> ah) {
if (!exists cx.body.newPath)
return RestHandler::make400("missing 'newPath' parameter for rename(%y)", h.path);
try {
rename(h.path, cx.body.newPath);
} catch (hash<ExceptionInfo> ex) {
return RestHandler::make400("rename (%y): %s: %s", h.path, ex.err, ex.desc);
}
return RestHandler::makeResponse(200, "OK");
}
}

Assuming this object corresponds to URL path /files/info.txt, the following requests are supported:

  • "GET /files/info.txt HTTP/1.1": calls the get() method above (no action)
  • "PUT /files/info.txt/chown;user=username HTTP/1.1": calls the putChown() method above (such arguments could also be passed in the message body; see Argument and Message Body Handling for more information)
  • "PUT /files/info.txt/chmod;mode=420 HTTP/1.1": calls the putChmod() method above (note that 420 = 0644)
  • "PUT /files/info.txt/rename;newPath=/tmp/old.txt HTTP/1.1": calls the putRename() method above
  • "PUT /files/info.txt?action=chown;user=username HTTP/1.1": calls the putChown() method above (such arguments could also be passed in the message body; see Argument and Message Body Handling for more information)
  • "PUT /files/info.txt?action=chmod;mode=420 HTTP/1.1": calls the putChmod() method above (note that 420 = 0644)
  • "PUT /files/info.txt?action=rename;newPath=/tmp/old.txt HTTP/1.1": calls the putRename() method above

Implementing Object Hierarchies

To implement a hisearchy of REST objects in the URL, each AbstractRestClass subclass adds static subclass references using the AbstractRestClass::addClass() method and can also reimplement the subClassImpl() method to return a child AbstractRestClass object representing the child object programmatically based on REST API arguments or the current application state.

If no such sub object exists, then the method should return NOTHING, which will cause a 404 Bad Request response to be returned by (to change this behavior, reimplement unknownSubClassError() in your specialization of RestHandler. Otherwise, if an exception occurs handling the request, a 409 Conflict response is returned; see Exception Handling in REST Calls for more information.

The following is an example of a class implementing a subClassImpl() method:

class WorkflowOrderRestClass inherits AbstractRestClass {
private {
# this holds the information or state of the REST object
hash<auto> wf;
}
constructor(hash<auto> n_wf) {
wf = n_wf;
}
# the name of the REST object
string name() {
return "orders";
}
# supports subclass requests by returning an object if the id is recognized
*AbstractRestClass subClassImpl(softint order_id, hash<auto> cx, *hash<auto> ah) {
*hash<auto> h = sysinfo.getOrderInfo(wf.id, order_id);
if (h) {
return new WorkflowOrderInstanceRestClass(h);
}
}
# supports \c GET requests on the object
hash<HttpHandlerResponseInfo> get(hash<auto> cx, *hash<auto> ah) {
return RestHandler::makeResponse(200, wf);
}
}
class WorkflowOrderInstanceRestClass inherits AbstractRestClass {
private {
# this holds the information or state of the REST object
hash<auto> oh;
}
constructor(hash<auto> n_oh) {
oh = n_oh;
}
# the name of the REST object
string name() {
return oh.order_instanceid;
}
# supports \c PUT requests where action=retry on the object
hash<HttpHandlerResponseInfo> putRetry(hash<auto> cx, *hash<auto> ah) {
return RestHandler::makeResponse(200, api.retryOrder(oh.order_instanceid));
}
# supports \c GET requests on the object
hash<HttpHandlerResponseInfo> get(hash<auto> cx, *hash<auto> ah) {
return RestHandler::makeResponse(200, oh);
}
}
Note
REST classes directly under the base handler object inheriting RestHandler can add subclasses by calling RestHandler::addClass() as well; the value of the AbstractRestClass::name() method provides the name of the subclass.

Argument and Message Body Handling

There are two ways to pass arguments to REST action methods:

  • 1: pass them as URI query arguments
  • 2: pass them as a hash in the message body

REST action methods have the following signature:

hash<HttpHandlerResponseInfo> method(hash<auto> cx, *hash<auto> ah) {}

In the usual case when only one of the above methods is used, then the argument information is copied to both places, making it easier for the Qore REST implementation to handle arguments in a consistent way regardless of how they were passed. This means arguments (including any action argument), can be passed in either the URI path as URI query arguments, or in the message body as a hash.

Therefore, the following cases are identical:

Request With URI Argument

PUT /files/info.txt/chmod;mode=420 HTTP/1.1
Accept: application/x-yaml
User-Agent: Qore-RestClient/1.0
Accept-Encoding: deflate,gzip,bzip2
Connection: Keep-Alive
Host: localhost:8001

Request With Arguments in the Message Body

PUT /files/info.txt/chmod HTTP/1.1
Accept: application/x-yaml
User-Agent: Qore-RestClient/1.0
Accept-Encoding: deflate,gzip,bzip2
Connection: Keep-Alive
Host: localhost:8001
Content-Type: application/json;charset=utf-8
Content-Length: 16

{ "mode" : 420 }
REST URI Arguments
REST URI arguments are available in the "ah" argument to the method. REST URI arguments are provided internally with the parse_uri_query function.

If no URI query argument were provided in the request, then the "ah" parameter is populated by the deserialized message body, if it is deserialized to a hash.
REST Message Body
The REST request message body is automatically deserialized and made available as "cx.body" in REST action methods.

If no message body was included in the client request, then any parsed URI parameters are copied to "cx.body".

For requests with long or complex arguments should send arguments as message bodies and not as serialized URI query arguments.
Note
The RestHandler class allows for HTTP GET requests to be served with a message body, but this is not compliant with HTTP 1.1 RFCs and therefore could lead to compatibility problems should this technique be used with other servers; see HTTP GET Requests With a Message Body for more information.

Exception Handling in REST Calls

Qore exceptions are serialized in a consistent way by the RestHandler class so that they can be recognized and handled appropriately by clients.

Qore exceptions are returned with a 409 "Conflict" HTTP return code with the message body as an ExceptionInfo hash.

RestHandler Release Notes

RestHandler v1.6.2

  • always set the socketobject request context property (issue 4666)

RestHandler v1.6.1

RestHandler v1.6

RestHandler v1.5.2

  • allow a logger to be set in validators (issue 4509)

RestHandler v1.5.1

  • improved error handling and sending error messages when exceptions contain non-serializable objects (issue 4501)

RestHandler v1.5

  • implemented support for allowing alternate REST method dispatch approaches to be implemented (issue 4478)

RestHandler v1.4

  • added missing make*() methods with REST responses to the RestHandler class

RestHandler v1.3.2

  • fixed a bug handling errors registering stream errors with a stream handler (issue 4291)

RestHandler v1.3.1

RestHandler v1.3

  • added the RestHandler::returnRestException() method that allows subclasses to determine how exceptions are handled (issue 3235)
  • updated to support alternative URI paths for actions so that an "action=xxx" argument is not needed; instead the action can be added to the end of the URI path so that "PUT path/xxx" can be used instead of "PUT path?action=xxx". Additionally, support for fast static REST subclass lookups was added by moving the addClass() method from the RestHandler class to the AbstractRestClass class. (issue 2994)

RestHandler v1.2.1

  • updated to return a 400 Bad Request error when REST schema validation fails on messages received (issue 2344)
  • updated to return a 400 Bad Request error when there are string encoding errors with messages received (issue 2398)
  • updated to return a 404 Not Found error when REST subclass does not exist (issue 2405)
  • updated to return a 400 Bad Request error when ENCODING-CONVERSION-ERROR occurs during request parsing (issue 2543)

RestHandler v1.2

  • added logic to allow sensitive data to be masked in log messages (issue 1086)
  • added RestHandler::handleExternalRequest() to allow for REST API methods to be called internally (i.e. without an HTTP call)
  • updated for complex types (issue 1724)
  • added support for REST schema validation
  • added default support for the HTTP "PATCH" method

RestHandler v1.1

  • added support for the HTTP OPTIONS method
  • return an error if an unsupported HTTP method is used in a REST call
  • fixed a type error in the ah member of RestHandler::AbstractRestStreamRequestHandler
  • fixed a bug where an error calling an internal nonexistent method would be reported with an incorrect error message
  • implemented support for notifying persistent connections when the connection is terminated while a persistent connection is in place
  • RestHandler::AbstractRestStreamRequestHandler is now the base abstract class for REST stream request handlers
  • send errors are now reported in the RestHandler::AbstractRestStreamRequestHandler object so they can be properly logged (issue 734)
  • unknown REST class errors with the base class are now reported consistently like all other such errors (issue 859)

RestHandler v1.0.1

  • added support for URL form encoded formatted message bodies (issue 1436)

RestHandler v1.0