3. Routing & Handling Requests¶
Web Server Gateway Interface or WSGI (PEP 333) is the glue between a Web Server and your Python Web application. The responding application simply has to be a Python callable (a python function or a class that implements the __call__
method). Each callable is passed the Web server environment and a start_response
.
Note
If you are unfamiliar with WSGI we recommend reading Armin Ronarcher‘s introduction to WSGI. We also have a great collection of Reference Material on Python Web development.
WSGI interfaces will generally handover requests that match a URL pattern to the mapped WSGI callable. From the callable is responsible for dispatching the request to the appropriate handler based on part of the URL, HTTP verb, headers or any other property of an HTTP request or a combination properties. This middle ware code is refered to as a request router and Prestans provides one of it’s own.
Prestans makes use of standard HTTP headers for content negotiation. In addition it uses a handful of custom headers that the client can use to control the Prestans based API’s behavior (features include Content Minification and Attribute Subsets for requests and responses). We’ll first introduce you to the relevant HTTP and how it effects your API requests followed by how you can handle API requests in prestans.
3.1. Serializers & DeSerializers¶
Serializers and DeSerializers are pluggable Prestans constructs that assist the framework in packing or unpacking data. Serialzier or deserializer handle content of a particular mime type and are generally wrappers to vocabularies already available in Python (although it possible to write a custom serializer entirely in Python). Serializers always write out a parseable version of models.
Prestans application may speak as many vocabularies as they wish; vocabularies can also be local to handlers (as opposed to applicaiton wide). You must also define a default format.
Each request must send an Accept
header for Prestans to decide the response format. If the registered handler cannot respond in the requeted format Prestans raises an UnsupportedVocabularyError
exception inturn producing a 501 Not Implemented
response. All Prestans APIs have a set of default formats all handlers accept, each end-point might accept additional formats.
If a request has send a body (e.g PUT
, POST
) you must send a Content-Type
header to declare the format in use. If you do not send a Content-Type
header Prestans will attempt to use the default deserializer to deserialize the body. If the Content-Type
is not supported by the API an UnsupportedContentTypeError
exception is raised inturn producing a 501 Not Implemented
response.
3.2. Routing Requests¶
Prestans is built right on top of WSGI and ships with it’s own WSGI Request Router. The router is responsible for parsing the HTTP request, setting up a HTTP response, setup a logger (if one wasn’t provided as part of the configuraiton), finally check to see if the API services the requested URL and hand the request over to the handler.
The handler is responsible for constructing the response and one return the router, asks the response to serialize itself. If an exception is raised (see Framework Design) is raised by the framework (because it couldn’t parse the request or response) or the handler, the router where appropriate, writes a detailed error trace back to the client.
Prestans verbosely logs events per API request. Your system logger is responsible for verbosity of the logger.
3.2.1. Regex & URL design primer¶
URL patterns are described using Regular expression, this section provides a quick reference to handy regex patterns for writing REST services. If you are fluent Regex speaker, feel free to skip this chapter.
Most URL patters either refer to collections or entities, consider the following URL scheme requirements:
/album/
- refers to a collection of type album/album/{id}
- refers to a specific album entity
Notice no trailing slashes at the end of the entity URL. Collection URLs may or may not have a URL slash. The above patterns can would be represented in like Regex as:
/album/(.*)
- For collection of albums/album/([0-9]+)
- For a specific album
If you have entities that exclusively belong to a parent object, e.g. Albums have Tracks, we suggest prefixing their URLs with a parent entity id. This will ensure your handler has access to the {id} of the parent object, easing operations like:
- Does referenced parent object exists?
- When creating a new child object, which parent object would you like to add it to?
- Does the child belong to the intended parent (Works particularly well with ORM layers like SQLAlchemy)
A Regex example of these URL patterns would look like:
/album/([0-9]+)/track/(.*)
/album/([0-9]+)/track/([0-9]+)
3.2.2. Using Request Router¶
Warning
since Prestans 2.1, URL’s are matched according to the WSGI environment variable 'PATH_INFO'
, this allows for the creation of mount-point agnostic paths. Users of Prestans versions < 2.1 who rely on the WSGIScriptAliasMatch
directive should replace them with WSGIScriptAlias and remove pattern matching from the first argument and adjust their URL route regex definitions to truncate the mount-point path
example upgrade to Prestans>=2.1:
WSGIScriptAliasMatch ^/api/(.*) /scripts/my_app.wsgi # replace with...
WSGIScriptAlias /api /scripts/my_app.wsgi
application = RequestRouter([
(r'/api/things', my.Handler), # replace with...
(r'/things', my.Handler)
])
The router is provided by the prestans.rest
package. It’s the keeper of all Prestans API requests, it (with the help of other members of the prestans.rest
package) parases the HTTP request, setups the appropriate handler and hands over control.
The router also handles error states raised by the API and informs the requesting client to what went wrong. These can include issues with parsing the request or exceptions raised by the handler (this is covered later in the chapter) while dealing with the request.
The constructor takes the following parameters:
routes
a list of tuples that maps a URL with a request handlerserializers
takes a list of serializer instances, if you omit this parameter Prestans will assign JSON as serializer to the API.default_serializer
takes a serializer instance which it uses if the client does not provide anAccept
header.deserializers
, a set of deserializers that the clients use via theContent-Type
header, this default toNone
and will result in Prestans using JSON as the default deserializer.default_deserializer
, default serializer to be used if a client doesn’t provide aContent-Type
header, defaults toNone
which results in Prestans using the JSON deserializer.charset
, to be used to parse strings, defaulted toutf-8
application_name
the name of your API,prestans
logger
, an instance of a Python logger, defaulted toNone
which results in Prestans creating a default logger instance.debug
, runs Prestans under debug mode (results in increased logging, error reporting), it’s defaulted toFalse
import prestans.rest
import myapp.rest.handlers
api = prestans.rest.RequestRouter(routes=[
(r'/([0-9]+)', myapp.rest.handlers.DefaultHandler)
],
serializers=[prestans.serializer.JSON()],
default_serializer=prestans.serializer.JSON(),
deserializers=[prestans.deserializer.JSON()],
default_deserializer=prestans.deserializer.JSON(),
charset="utf-8",
application_name="music-db",
logger=None,
debug=True)
The router is a standalone WSGI application you pass onto server environment.
If your application prefers a diaclet other than JSON as it’s default, ensure you configure this as part of the router. It’s recommended that the default diaclet for serialization and deserialization is the same.
The default logger uses is a configured Python Logger, it logs in detail the lifecycle of a request along with the requested URL. Refer to documentation on how to configure your system logger to control verbosity.
Once your router is setup, Prestans is ready to route requests to nominated handlers.
If were deploying under mod_wsgi, your the above would be the contents of your WSGI file. mod_wsgi requires the endpoint to be called application
. The WSGI configuration variable might look something like.
Note
since 2.1
WSGIScriptAlias /api /srv/musicdb/api.wsgi
Under AppEngine, if this above was declared under a script named entry.py
, you would reference it in app.yaml
as follows:
- url: /api/.*
script: entry.api
login: required
3.3. Handling Requests¶
REST requests primarily use the following HTTP verbs to handle requests:
HEAD
to check if the entity the client has is still currentGET
to retrieve entitiesPOST
to create a new entityPUT
to update an entityPATCH
to update part of an entityDELETE
to delete an entity
Prestans maps each one of these verbs to a python function of the same name in your REST handler class. Each REST request handler in your application derives from prestans.rest.RequestHandler
. Unless your handler overrides the functions get
, post
, put
, patch
, delete
the base implementation tells the Prestans router that the requested end point does not support the particular HTTP verb.
Your handler must accept an equal number of parameters as defined the router regular expression.
Our Regex premier highlights the use of two handlers per entity, one deals with collections the other entities. APIs generally let clients get a collection of entities, add to a collection and get a particular entity, update an entity or delete an entity. The later require an identifier for the entity, where as the collection does not.
/album/([0-9]+)/track/(.*)
/album/([0-9]+)/track/([0-9]+)
This is no way says that your API can’t provide an endpoint to delete all entities of a type or update a collection of entities, in which instances your collection handler would implement the appropriate HTTP verb handlers.
A typical collection handlers would typically look like (implementing GET
and POST
and does not require an identifier):
import prestans.rest
import prestans.parser
import myapp.rest.models
class MyCollectionRESTRequestHandler(prestans.rest.RequestHandler):
__parser_config__ = prestans.parser.Config(
GET=prestans.parser.VerbConfig(
response_template=prestans.types.Array(element_template=myapp.rest.models.Track())
),
POST=prestans.parser.VerbConfig(
body_template=myapp.rest.models.Track(),
response_template=myapp.rest.models.Track()
)
)
def get(self):
... return a collection of entities
def post(sef):
... add a new type
A typical entity handler would look like (implementing GET
, PUT
and DELETE
expecting an identifier):
import prestans.rest
import prestans.parser
import myapp.rest.models
class MyEntityRESTRequestHandler(prestans.rest.RequestHandler):
__parser_config__ = prestans.parser.Config(
GET=prestans.parser.VerbConfig(
response_template=myapp.rest.models.Track()
),
PUT=prestans.parser.VerbConfig(
body_template=myapp.rest.models.Track(),
)
)
def get(self, track_id):
... return an individual entity
def put(self, track_id):
... update an entity
def delete(self, track_id):
... delete an entity
Notice that since deleting an entity only requires an identifier, and does not have to parse the body of a request. The update request can also choose to use attribute filters to pass in partial objects.
Note
At this point if you’d rather learn about how to parse requests and responses, then head to the chapter on Validating Requests & Responses. This chapter continues to talk about how handlers work assuming you are going to read the chapter on validation shortly after.
Each handler allows accessing the environment as follows:
self.request
is the parsed request based on the rules defined by your handler, this is an instance ofprestans.rest.Request
self.response
is response Prestans will eventually write out to the client, this is an instance ofprestans.rest.Response
self.logger
is an instance of the logger the API expects you to write any information to, this must be an instance of a Python loggerself.debug
is a boolean value passed on by the router to indicate if we are running in debug mode
Each request handler instance is run in the following order (all of these methods can be overridden by your handler):
register_seriailzers
is called on the handler, this allows the handler to a list of additional serializers it would like to useregister_deserializers
is called on the handler, this allows the handler to a list of additional deserializers it would like to usehandler_will_run
is called, perform any handler specific warm up acts here- The function that corresponds to the requests HTTP verb is called
handler_did_run
is called, perform any handler specific tear downs here
prestans.rest.RequestHandler
can be subclassed as your project’s Base Handler class, this generally contains common code e.g getting access to a database sessions, etc. A SQLAlchemy specific example would look like:
import prestans.rest
import prestans.parser
import myapp.rest.models
class Base(prestans.rest.RequestHandler):
def handler_will_run(self):
self.db_session = myapp.db.Session()
self.__provider_config__.authentication = myapp.rest.auth.AuthContextProvider(self.request.environ, self.db_session)
def handler_did_run(self):
myapp.db.Session.remove()
@property
def user_profile(self):
return self.__provider_config__.authentication.get_current_user()
@property
def auth_context(self):
return self.__provider_config__.authentication
Note
You’d typically place this in myapp.rest.handlers.__init__.py
and place all your handlers grouped by entity type in that package.
3.4. Constructing Response¶
The end result of all handler call is to send a response back to the client. This can be as simple as a status code, or as elaborate as a group of entities. Prestans is unforgiving (unless requested otherwise) while accepting requests and writing responses.
In accordance with the REST standard, each handler must declare what sort of entities (if any) the handler will return if it successfully processes a request. We will cover error scenarios later in this chapter.
Declaration of response types are defined as part of your parser configuration per HTTP verb. Handlers typically return a collection of entities, defined as:
import prestans.rest
import prestans.parser
import prestans.types
import myapp.rest.models
class MyEntityRESTRequestHandler(prestans.rest.RequestHandler):
__parser_config__ = prestans.parser.Config(
GET=prestans.parser.VerbConfig(
response_template=prestans.types.Array(element_template=myapp.rest.models.Album())
)
)
def get(self):
albums = prestans.types.Array(element_template=myapp.rest.models.Album())
albums.append(myacc.rest.models.Album(name="Journeyman", artist="Eric Clapton"))
albums.append(myacc.rest.models.Album(name="Dark Side of the Moon", artist="Pink Floyd"))
return albums
or an individual entity, defined as:
import prestans.rest
import prestans.parser
import myapp.rest.models
class MyEntityRESTRequestHandler(prestans.rest.RequestHandler):
__parser_config__ = prestans.parser.Config(
GET=prestans.parser.VerbConfig(
response_template=myapp.rest.models.Album()
)
)
def get(self, album_id):
return myapp.rest.models.Album(name="Journeyman", artist="Eric Clapton")
More often than not, the content your handler sends back, would have been read queried from a persistent data store. Sending persistent data verbatim nearly never fits the user case. A useful API sends back appropriate amount of information to the client to make the request useful without bloating the response. This becomes a cases by case consideration of what a request handler sends back. Sending out persistent objects verbatim could sometimes pose to be a security threat.
Prestans requires you to transform each persistent object into a REST model. To ease this tedious task Prestans provides a feature called Data Adapters. Data Adapters perform the simple task of converting persistent instances or collections of persistent instances to paired Prestans REST models, while ensuring that the persistent data matches the validation rules defined by your API.
Data Adapters are specific to backends, and it’s possible to write your own your backend specific data adapter. All of this is discussed in the chapter dedicated to Data Adapters.
3.4.1. Minifying Content¶
Prestans tries to make your API as efficient as possible. Minimizing content size is one of these tricks. Complimentary to Attribute Filters (which allows clients to dynamically turn attributes on or off in the response) is response key minification.
This is particularly useful for large amounts of repetitive data, e.g several hundred order lines.
Setting the Prestans-Minification
header to On
is all that’s required to use this feature. This is a per request setting and is set to Off
by default.
Prestans also sends Prestans-Minification-Map
header back containing a one to one map of the original attribute names it’s minified counterpart.
Note
Our Google Closure extensions provides a JSON REST Client, which can automatically unpack minified requests to fully formed Prestans client side models.
3.4.2. Serving Binary Content¶
It’s perfectly legitimate for your REST handler to return binary content. Prestans provides a built in model to assign to your handler’s VerbConfig
.
Your handler must return an instance of prestans.types.BinaryContent
, and Prestans will do what’s right to deliver binary content.
Instances of BinaryContent
accept the following parameters:
mime_type
, the mime type of the file that you are sending backfile_name
, the filename that Prestans is to send back in the HTTP header, this is what the browser thinks the name of the file is. File names can be generated by applications or they might have been stored as meta information when the file was uploaded by the useras_attachment
, tells Prestans if the file is to be delivered as an attachment (forces the user to save the file) or deliver it inline (generally opens in the browser).contents
, binary contents that you’ve read up from disk or generated.
import prestans.types
class Download(st.cs.rest.handlers.Base):
__parser_config__ = prestans.parser.Config(
GET=prestans.parser.VerbConfig(
response_template=prestans.types.BinaryResponse()
)
)
def get(self, document_id):
file_contents = open(file_path).read()
self.response.status = prestans.http.STATUS.OK
self.response.body = prestans.types.BinaryResponse(
mime_type='application/pdf',
file_name='invoice.pdf',
as_attachment=True,
contents=file_contents)
If you set as_attachment
to False the file will be delivered inline into the browser. It’s up to the browser to handle the content properly.
3.5. Raising Exceptions¶
As alluded to in our Thoughts on API design chapter, Prestans provides two distinct set of Exceptions. The first raised if you’ve configured your API incorrectly and the later used to send back meaningful error messages to the requesting client.
This section deals with Exceptions that Prestans expects you to use to raise meaningful REST error messages. These are generally caused by the client sending you an inappropriate e.g the logged in user is not allowed to access or update an entity, or something simply not being found on the persistent data store.
Note
PEP 008 recommends that Exceptions that are errors should end with the Error suffix.
You do not have to raise exceptions for request and response data validation. If the data does not match the rules defined by your models, parameter sets or attribute filters, it’s prestans’ responsibility to graceful fail and respond back to the client.
3.5.1. Unsupported Vocabulary or Content Type¶
Prestans uses the Accept
and Content-Type
HTTP headers to negotiate the format the client is sending their request as or the format they expect the response in. Prestans ships with a standard set of vocabularies (serialization formats) and allow you to add your own. Prestans automatically adjusts the serializer or de-serializer to use based on the mime types. If Prestans is unable to service the request, the following exceptions are raised:
UnsupportedVocabularyError
raised if Prestans can’t find a suitable serializer for the requested mime type in theAccept
headerUnsupportedContentTypeError
raised if Prestans can’t find a suitable de-serializer for the requested mime type in theContent-Type
header
If these exceptions are raised as part of parsing the request Prestans generates an appropriate message using the default serializer (internally set to JSON by default).
3.5.2. Configuration Exceptions¶
Along with Python’s TypeError Prestans raises the following exceptions if you’ve configured portions of your application incorrectly.
InvalidType
raised if the Python typed passed in for validate is incorrect.ParseFailedError
raised if the data type fails to evaluate the value as an appropriate Python data typeInvalidMetaValueError
raised if you’ve passed a unacceptable configuration option for an attributeMissingParameterError
raised if a required parameter for an attribute is missingUnregisteredAdapterError
raised if the Data Adapters registry can’t locate a REST to persistent map
3.5.3. Data Validation Exceptions¶
Prestans raises the following exceptions (See PEP 008 for naming conventions of Exceptions) if data passed through Prestans types fails to validate. If the validation fails as part of the request Prestans captures the stack trace and responses to the client with an appropriate error code. If a request fails to parse your handler code will not be executed. If the exception is raised a result of your code (e.g using Data Adapters or writing responses) Prestans will halt the execution and write the error message to the logger.
RequiredAttributeError
raised if an attribute is required and an appropriate value wasn’t provided, note that you can relax these by using attribute filtersLessThanMinimumError
raised if the value provided for an attribute is less than the floorMoreThanMaximumError
raised if the value provided for an attribute is greater than the ceilingInvalidChoiceError
raised if the value provided is not defined in the set of choices for the attributeUnacceptableLengthError
raised if the value provided is longer than the acceptable lengthInvalidFormatError
raised if the value provided does not pass the regular expression format
3.5.4. Parser Exceptions¶
Prestans raises the following exceptions when parsing data. If the exception is raise while parsing a request Prestans will fail gracefully by responding to the client with an error message and a track trace. If the exception is raised while your handler returns a response Prestans will stop the execution and expect you fix the issue.
Note
Since all data is strictly validated your application should to maintain consistent data at all times.
UnimplementedVerbError
raised if a client requests an HTTP verb that an end point does not handleNoEndpointError
raised if a client requests an endpoint that does not existsAuthenticationError
raised if the client does not have an authenticated session and the handler requires them to do soAuthorizationError
raised if the logged is user fails to pass the authorisation rulesSerializationFailedError
raised if serialization of the data fails, would only happen if the handler passed a data type that cannot be handled by the nominated serializer.DeSerializationFailedError
raised if deserialization failed, this would only happen if the client passed data that cannot be parsed by the nominated deserializerAttributeFilterDiffers
raised if the attribute filter differs from the nominated response templateInconsistentPersistentDataError
3.5.5. Handler Exceptions¶
Prestans make the following exceptions available for your handler to use. They are associated to error codes defined in the HTTP specification and when raised Prestans gracefully returns to the client with an appropriate error message and where applicable a stack trace. Each exception allows you to pass a custom error message which is returned to the client. Prestans will also set the appropriate HTTP error code in the response header.
ServiceUnavailable
to be raised if the service called cannot satisfy the request at the present time, this could be caused due to exhausted quotas on cloud services or a database services that may have failed to respondBadRequest
raised when the client placed an inappropriate request, a typical example is that the data Prestans parsed is in the right format but your service is unable to process the combination.Conflict
raised when the data provided to the end point is valid but conflicts with the business logicNotFound
raised when a requested entity is not found, for example the entity ID provided as part of the request is valid in format but does not exists on the systemUnauthorized
raised if the particular user is not allowed to access an entity or related childrenMovedPermanently
raised if the particular endpoint has moved, this should cause the client to request the newly appointed URLPaymentRequired
raised if your service requires the user to subscribe to the service and their subscription has expiredForbidden
raised if the the particular entity the client requested should not be accessed by the currently logged in user