Webservice - noxrepo/nox-classic GitHub Wiki

Table of Contents

Overview

This module implements the component providing the web services interface for NOX applications. Other NOX applications use this module to register to handle specific web services requests and generate appropriate responses.

Background

The NOX web services are intended to implement a RESTful web service API, roughly following the Resources Oriented Architecture described in RESTful Web Services by Leonard Richardson & Sam Ruby. See that book for a detailed explanation. Briefly however, the key ideas are:

  • Addressability: Every interesting piece of data and algorithm exposed by the application can be addressed as a separate resource. URIs are the method of ddressing resources in HTTP, so effectively this means each has its own URI. The message body content of a request is NOT used to determine the data to be affected. Ideally the URIs are descriptive and human-readable.
  • Statelessness: Every request happens in isolation. The server does not base future actions on past client behavior except as that behavior affected the resources exposed by the server. In practice, this means that the application should not uses session variables in dealing with requests.
  • Connectedness: Where possible, the application should return links to other related resources in the representations it serves to the client, allowing the client to navigate the available resources without having to understand all possible URIs.
  • Uniform Interface: The web service should have a uniform resource addressing and representations. Uniform addressing is accomplished by using the standard HTTP request methods in their "natural" interpretation along with the resource-specific URIs. The request methods typically used are:
    GET: Retrieve a respresentation of a resource.
    POST: Create a new resource that doesn't have a known URI before it is created.
    PUT: Modify an existing resource or create a new resource at a URI known before the resource is created.
    DELETE: Delete an existing resource.

Uniform representations is accomplished by representing resources using a standard encoding. In many web services, this is XML. For NOX, because the primary client for web services is expected to be web browsers, Javascript Object Notation (JSON) is used. Error responses are also made uniform by using the standard HTTP response codes.

Registering for Requests

Applications implement a web services interface by registering to respond to specific requests with the information appropriate to that application. To register for a URI, an application must follow these steps:

  • Resolve the webservices component
       ws = component.resolve(str(nox.webapps.webserver.webservice))
  • Get an object representing a specific version of a web services interface. This is to allow for future incompatible changes to the interface should they be required.
       v1 = ws.get_version("1")
  • Call the register_request method on that object.
       v1.register_request(handler, request_method, path_components, doc)

The parameters given to register_request describe a set of requests (HTTP request method + URI paths) for which the given handler callback will be called and provide some documentation about the purpose of the request. With the exception of the path_components parameter they are straightforward:

    handler: The callback function to be called to handle the request.
             It will be called with two parameters, the HTTP web server
             request object and a dictionary of data extracted from the
             URI as described by the path_components parameter.
    request_method: The HTTP request method (for example, GET, PUT,
             POST, etc.)
    doc: A string explaining the purpose of the request.

The path_components parameter describes the URIs to be handled. It contains a sequence (list, tuple, etc.) of WSPathComponent subclass instances. The simplest such subclass is WSPathStaticString. This code:

    path_components = ( WSPathStaticString("abc"),
                        WSPathStaticString("xyz") )
    v1.register_request(h, "GET", path_components, "Some doc")

Will register the "h" callback function to be called for the request:

    GET /ws.v1/abc/xyz

While often required, matching on static strings could have been handled by a much simpler mechanism. The reason for the current implementation is to support extracting and verifying dynamic data from URIs. As an example, consider a request to delete a user login account. The form of the request could be:

    DELETE /ws.v1/user/<user name>

where <user></user> represents a valid existing login name. If there is a "userdb" object that provides the method "isValidLogin" to determine if a login is valid, a WSPathComponent subclass to extract and verify the login name can be implemented as:

        def __str__(self):
            return "&lt;login&gt;&lt;/login&gt;"
    
        def extract(self, path_component_str, data):
            if self.userdb.isValidLogin(path_component_str):
                return WSPathExtractResult(value=path_component_str)
            else:
                e = "'%s' is not a valid login name." % path_component_str
                return WSPathExtractResult(error=e)

To handle the URI we would register it as:

    path_components = ( WSPathStaticString("user"),
                        WSPathUserLogin(userdb) )
    v1.register_request(d, "DELETE", path_components, "Delete user login.")

The method of WSPathUserLogin is particularly important because it is used in several ways:

  • The request handler constructs a tree of request paths for efficient URI path matching. It assumes that two WSPathComponent subclass instances that return the same value for () do the same thing and will keep only one of the multiple instances that might have been registered for different paths that share the same prefix. Therefore it should be different if the class behaves differently depending on how it is initialized. See WSPathStaticString for an example.
  • The request handler also auto-generates informative error messages and a documentation page describing valid request templates. Conceptually, it constructs the user displayed path by evaluating the python expression:
            "/" + "/".join([str(pc)])

For this reason, by convention subclasses that extract dynamic data should return a string enclosed in angle brackets ('<', and '>') as done in the user login example above.

  • The dictionary of data extracted from the URI is keyed by this value as well. This dictionary is passed to both extract() method calls on subsequent path components and to the final callback function. For this reason, the value returned should be predictable, typically a constant string or a string that varies in a predictable way.

Handling Requests

Once the request handler has determined the correct callback function to handle a request it calls it with the web server request object and the extracted data dictionary. The callback function must generate the response data to be sent to the client. One method of doing so is to return that data immediately. A simple example is:

    def hello_user_handler(request, data):
        request.setHeader("Content-Type", "text/plain")
        return "Hello %s!\n" % (data["&lt;login&gt;&lt;/login&gt;"],)

If this were registered as follows (using the previously defined WSPathUserLogin class):

    pc = ( WSPathStaticString("hello"),
           WSPathUserLogin(userdb) )
    v1.register_request(hello_user_handler, "GET", pc, "Say hello to &lt;user&gt;&lt;/user&gt;.")

Then the request:

    GET /ws.v1/hello/john_smith

would result in a text document containing the single line:

    Hello john_smith!

Assuming of course, that john_smith was a valid login name. If not, the request handler would return a not found error with information about why the request could not be satisified without calling the handler callback at all.

If the data to be returned is generated from a deferred or other asynchronous mechanism, this is not possible. In that case, the handler should return NOT_DONE_YET and output the content using one or more calls of request.write() method followed by a call to the request.finish() method. For example:

        def ok(self, res):
            self.request.write("The result was successful.")
            self.request.finish()
    
        def do_deferred(self):
            d = someDeferredOp()
            d.addCallback(self.ok)
    
    def deferred_handler(request, data):
        DeferredExample(request).do_deferred()
        return NOT_DONE_YET

This module provides several functions to facilitate handling requests. First, there are a set of general functions to generate uniform HTTP error responses of the same names. These are:

    badRequest, conflictError, and internalError

There are also a set of special purpose functions for other error codes that are typically used by the request handler itself and not the handler callbacks, but may be needed by the callbacks in special circumstances. These include:

    notFound, methodNotAllowed, unauthorized, and forbidden

The last of these (forbidden) is related to authorization checking. This *is* the responsibility of the handler callback. The authorization system is based on sets of capabilities identified by strings registered by the application. The handler callback should check the client is logged in as a user with the required capabilities to perform the request operation. Rather than doing its own tests and calling forbidden with its own error message, the handler callback will typically check permissions using the authorization_failed function. A simple example is:

    def simple_access_check_handler(request, data):
        if authorization_failed(request, [set("capability1",]):
            return NOT_DONE_YET
        request.setHeader("Content-Type", "text/plain")
        return "Congratulations, you are authorized to see this page!"

Finally, the uniform resource representation for NOX applications is Javascript Object Notation (JSON). By default, the request handler sets the content-type of the response to 'application/json' before calling the handler callback. The callback shouldn't generally need to overide this but can if required. To generate JSON output, it is recommended that the callback handler use the simplejson library.

The requests with the PUT and POST methods often contain a message body that will also be JSON encoded. To create a corresponding python object representation (of nested dictionaries, lists, and literal values) this module provides the json_parse_message_body function. This function will ensure the message body has the correct content type and that it parses as a valid JSON representation, outputting uniform error messages if these tests fail. It returns the parsed object if it succeeds or None if it fails. In the latter case it has already generated the error response for the client so the handler callback should just return NOT_DONE_YET, with something like:

    # ... setup at the beginning of the handler callback
    content = json_parse_message_body(request)
    if content == None:
        return NOT_DONE_YET
    # further validate rep and continue processing here...

Testing an Application Web Services Interface

If NOX is started with just the web services component being developed and the webservice_testui component, a web page can be accessed at http://127.0.0.1:8888/ that provides a way to specify the request method, URI, and message body of test requests, submit the request, and view the result returned by the server. This is the easiest way to test the implementation.

⚠️ **GitHub.com Fallback** ⚠️