Requests and responses

When a web request arrives at the front-end nginx server, it’s first subjected to the url rewriting. After that, the destiny of the request is decided as follows:

  • if the request is GET or POST, and the request url starts with /_, it’s routed to the front webapp
  • if the request is GET and the url starts with /gws-client, it’s routed to the app/www folder, which contains our client. This folder is populated by the client build
  • otherwise, if the request is GET, it’s served by nginx itself from the configured web root folder
  • otherwise (POST and other request methods), it’s a 400 bad request error

In simpler words, static content is being served unless the request url starts with an underscore.

When a GET request lands in the web app, it’s expected to provide parameters, either in the usual way (e.g. the query string), or encoded in the path, which is assumed to be like /parameter/value/parameter2/value2.... For example, this url

/_/cmd/mapHttpGetBox/layerUid/test.one?bbox=11,22,33,44

results in this

{
    cmd: "mapHttpGetBox"
    layerUid: "test.one"
    bbox: "11,22,33,44"
}

When processing a POST request, the payload is expected to be in JSON or MsgPack format, depending on the Content-Type, and to have the following structure

{
    cmd: "actionName",
    params: {
        // parameters as specified in the respective action handler
    }
}

Once the parameters structure has been parsed, the webapp takes the cmd parameter and determines the action module (from ext/actions) and the command handler method, for example

mapHttpGetBox     => module=map,     method=http_get_box
assetHttpGetPath  => module=asset,   method=http_get_path
printerSnapshot   => module=printer, method=api_snapshot

If the module doesn’t exist or is not configured for the current application or project, or the current user doesn’t have a permission to use the module, the request is rejected with the error 404 or 403.

Now, the webapp uses the specifications generated by make spec to ensure the parameters names and types match the structure declared in the action method. For JSON/MsgPack requests, the validation is strict (that is, a property declared as int should actually be an integer), http requests use “soft” validation (e.g. if an int is required, the parameter value can be a numeric string). If the validation fails, the request is rejected with the error 400.

After the validation completes and the parameters have been converted, the webapp picks the Object object from the respective action module and invokes the command method with two arguments: the request object and the parameters object.

The command method is supposed to return a Response object or to raise a HTTPException as defined is web/error.py. Returns of type HttpResponse or FileResponse are sent to directly to the client, other responses are encoded in the same format as the request (JSON or MsgPack).

Illustration

Here’s a code snippet that illustrates the above concepts

"""Example action object that provides an API method `hello`, which can be invoked in the client as `exampleHello`.

The code must be placed in gws/ext/action/example/__init__.py

"""

# import the generic Action object

import gws.common.action

# import web errors

import gws.web.error

# import our types

import gws.types as t

# define a Config object for this action
# this object will be automatically checked when the configurator encounters `action`: `{type: example}`

class Config(t.Config):
    """Configuration for the Example action"""

    helloString: str = "Hi"

# define the parameters structure for the `hello` command

class HelloParams(t.Params):
    color: str  #: color for the message

# define the response structure for `hello`

class HelloResponse(t.Response):
    message: str

# define the action object. It extends the generic action Object which is an IObject

class Object(gws.common.action.Object):

    # the mandatory configuration method

    def configure(self):
        # it's imperative to invoke super().configure() every time
        super().configure()

        # get the value of a configuration parameter and save it for the later use
        self.hello = self.var('helloString')

    # request handler method for `hello`
    # all action handlers accept the request object and parameters structure

    def api_hello(self, req: t.IRequest, p: HelloParams) -> HelloResponse:

        # check the params

        if p.color == 'red':
            # red is not implemented
            raise gws.web.error.NotImplemented()

        # use the request, the params and the preconfigured value to create a message

        message = f'<font color={p.color}>{self.hello}, {req.user.display_name}</font>'

        # create and return the response object

        return HelloResponse(message=message)