Request and response objects
Reading input, building responses and channel-specific metadata in every service.
Every service invocation comes with two objects - self.request is what the service received and self.response is what it sends back. Both always exist and both can be empty, it is perfectly fine for a service to accept no input and to produce no output.
Overview
self.request.input is the incoming message, parsed - a dict-like object for JSON, the raw text for other formats. self.request.raw is the message exactly as it arrived on the wire.
Read input to work with the data and read raw when you need the exact bytes as received, e.g. for checksums or signatures.
Both attributes work the same on every channel - REST, AMQP, Kafka, IBM MQ, the scheduler or any other. Channel-specific details, such as HTTP headers or AMQP delivery metadata, have their own sections further down.
Reading input
On JSON channels, self.request.input is the parsed request. Both dot access and dict access work:
# -*- coding: utf-8 -*-
# Zato
from zato.server.service import Service
class CreateOrder(Service):
def handle(self):
customer_id = self.request.input.customer_id
quantity = self.request.input['quantity']
URL query string and path parameters are merged into input as well:
class GetOrder(Service):
def handle(self):
order_id = self.request.input.order_id # From the URL path
region = self.request.input.region # From the query string
# The channel's URL path is /api/orders/{order_id}
curl "http://localhost:11223/api/orders/ORD-123?region=north"
What happens if a key does not exist? Reading it raises an AttributeError at the very line that reads it, naming both the missing key and the keys that do exist:
To read a key that may or may not be there, check for it first:
On non-JSON channels, such as EDIFACT or HL7 ones, input is the incoming text exactly as received:
class ProcessInterchange(Service):
def handle(self):
# 'UNB+UNOC:3+SENDER+RECIPIENT+260721:0130+REF-0001'...
interchange = self.request.input
Declared input
Input can also be declared up front. A declaration means that:
- The declared names are parsed into
inputbeforehandleruns - Required names are enforced - a request without one is rejected with an error naming the missing element
- Reading an optional name that was not sent gives
None - The declarations feed the OpenAPI documentation generated for the service
A declaration is a list of names and a leading minus means the name is optional:
class GetProfile(Service):
input = 'customer_id', '-priority'
def handle(self):
customer_id = self.request.input.customer_id
priority = self.request.input.priority # None when not sent
When a required name is missing, the request never reaches handle - it is rejected with an error naming the element, e.g. Missing required input element: customer_id.
The full rules - the types a name implies, output declarations and models side by side - are on the declaring input and output page.
Input can also be a data model, in which case input is an instance of that model:
# -*- coding: utf-8 -*-
# stdlib
from dataclasses import dataclass
# Zato
from zato.common.marshal_.api import Model
from zato.server.service import Service
@dataclass(init=False)
class CreateOrderRequest(Model):
customer_id: str
quantity: int
class CreateOrder(Service):
input = CreateOrderRequest
def handle(self):
# self.request.input is a CreateOrderRequest instance
customer_id = self.request.input.customer_id
Raw requests
self.request.raw is the message exactly as received, str or bytes, before any parsing. Signature verification and checksums must use raw, never input - input is the parsed form and re-serializing it will not reproduce the original bytes:
class ProcessWebhook(Service):
def handle(self):
signature = self.request.http.headers['x-signature']
raw_body = self.request.raw
# Compute the HMAC over raw_body and compare it with the signature
The webhooks chapter shows a complete signature verification example.
Channel context - HTTP
Services invoked over HTTP receive their channel context through self.request.http. Each capability below is available on any HTTP channel.
Read a query string parameter:
Read a path parameter - for a channel whose URL path is /api/orders/{order_id}:
Read the HTTP method, e.g. to serve GET and POST from one service:
def handle(self):
if self.request.http.method == 'GET':
self.response.payload.action = 'read'
else:
self.response.payload.action = 'write'
Read a header - header names are lower-cased and use dashes:
Read form data from a multipart request:
Channel context - AMQP
Services invoked over AMQP receive their channel context through self.request.amqp. The attribute holds the delivery itself and lets the service acknowledge or reject it:
class ProcessDelivery(Service):
def handle(self):
amqp = self.request.amqp
# The message as delivered by the broker
routing_key = amqp.msg.delivery_info['routing_key']
app_headers = amqp.msg.headers
# Acknowledge or reject the delivery explicitly ..
amqp.ack()
# .. or amqp.reject() to send it back.
Calling ack or reject is optional - when the service ends without either, the channel applies its own acknowledgment mode automatically. Call them yourself only when the decision has to be made mid-flight.
Channel context - queue bridge headers
Services invoked through queue bridge channels - Kafka and IBM MQ - receive broker metadata through self.request.headers, a plain dict. For IBM MQ these are the MQMD and MQRFH2 fields. One common use is routing on a header:
class RouteMessage(Service):
def handle(self):
message_type = self.request.headers['message_type']
if message_type == 'order':
self.invoke('orders.process', self.request.input)
else:
self.invoke('audit.store', self.request.input)
Channel context - SOAP
On SOAP channels the operation element arrives pre-unwrapped - input holds the operation's content and no envelope handling is needed in services. The SOAP tutorial covers it end to end.
Reference
Attributes available to all services, regardless of the channel:
| Attribute | Description |
|---|---|
| self.request | Along with self.channel, one of the main attributes describing incoming messages |
| self.request.input | The incoming message, parsed - see Reading input and Declared input |
| self.request.raw | The message exactly as received, str or bytes - see Raw requests |
| self.request.cid | Correlation ID for the current request - useful for logging and tracing |
| self.request.data_format | Data format of the request, e.g. 'json' or 'xml' |
| self.request.transport | Transport type used for the request |
| self.channel | Along with self.request, describes data and metadata about incoming messages. Unlike self.request, this attribute is the same for all requests coming through the same channel, i.e. it describes details of the channel itself rather than each individual message received. |
| self.channel.id | Unique ID of the channel |
| self.channel.name | Name of the channel the request was received through |
| self.channel.type | Type of the channel - will be equal to one of the constants in zato.common.CHANNEL |
| self.chan | Alias to self.channel |
| self.channel.security | Describes a security definition attached to the channel, if any is at all |
| self.channel.security.name | Name of the security definition |
| self.channel.security.username | Username used to invoke the channel, if applicable for a particular security type |
| self.channel.security.type | Type of the security definition - will be equal to one of the constants in zato.common.SEC_DEF_TYPE |
| self.channel.sec | Alias to self.channel.security |
HTTP-specific attributes, each shown in an example in Channel context - HTTP:
| Attribute | Description |
|---|---|
| self.request.http | The attribute to use to access HTTP-specific information |
| self.request.http.method | HTTP method used to invoke the service |
| self.request.http.GET | All GET parameters as a Bunch object, each value is either an exact one received or a list of values if there was more than one for a given key |
| self.request.http.POST | All POST parameters as a Bunch object, each value is either an exact one received or a list of values if there was more than one for a given key. Populated for channels whose data format is form data and for channels with no data format at all. |
| self.request.http.path | URL path that the request was received through, e.g. /customer/123 in "https://localhost:17010/customer/123"; the value does not include a query string |
| self.request.http.params | A dict-like object with URL path parameters, e.g. the value of {customer_id} from a channel mounted at /api/customers/{customer_id}. Query string parameters are in self.request.http.GET. Available even if data models are not used. |
| self.request.http.headers | All HTTP headers as a Bunch object, names lower-cased and dash-joined, e.g. 'x-api-key' |
| self.request.http.user_agent | The User-Agent header from the HTTP request |
| self.request.http.get_form_data() | Returns form data from multipart requests as a dictionary |
| self.wsgi_environ | While not belonging directly to self.request.http, each service can always have access to the full WSGI dictionary of data and metadata about the request |
More information
- Consult the dedicated chapter with programming examples for more details.
- The message building examples show the dot-access pattern in full
- To learn more about data models, click here
Overview
All services produce responses through self.response.payload. What you assign decides what goes to the wire:
- Dot access on the payload builds a nested structure of any depth and it serializes to the channel's data format
- A dict replaces anything built so far and serializes as it is
- A list of dicts becomes an array on the wire
- A string passes through to the wire exactly as it is
- A model instance serializes from its declared fields
- A payload that was never assigned nor built produces an empty response body
Output can also be declared, either as a list of names or as a data model, and the payload then follows the declared shape - the sections below cover each case. The message building examples show the dot-access pattern in full.
Free-form responses
With zero declarations, dot access on the payload builds nested structures of any depth and the assignments serialize to JSON in the order they were made:
def handle(self):
self.response.payload.customer.name = 'John Doe'
self.response.payload.customer.address.city = 'Amsterdam'
A list can be assigned under any nested name too:
def handle(self):
self.response.payload.order.lines = [
{'sku': 'AB-12', 'quantity': 2},
{'sku': 'CD-34', 'quantity': 1},
]
A dict can be assigned as a whole and it replaces anything built so far:
The same goes for a list of objects:
def handle(self):
data = [
{'id':123, 'name': 'John Doe'},
{'id':456, 'name': 'Jane Xi'},
]
self.response.payload = data
Assigning a string directly is always possible too, e.g. as a result of manual serialization - strings pass through to the wire exactly as they are:
A payload that was never assigned nor built produces an empty response body - a service with an empty handle returns nothing rather than an empty JSON object.
Declared output names
Declaring output names makes the payload accept those names only - assigning any other name raises an error at the very line that assigns it, naming both the unknown name and the declared list, so typos never reach the wire. The shape below each declared name stays open and builds through the same dot access:
class GetCustomer(Service):
output = 'customer'
def handle(self):
self.response.payload.customer.name = 'John Doe'
self.response.payload.customer.address.city = 'Amsterdam'
# This would raise an error - 'status' is not among the declared names
# self.response.payload.status = 'active'
A dict assigned as a whole keeps the declared names only - anything else in the dict is dropped, which makes it safe to pass internal dicts through without leaking fields:
class GetOrder(Service):
output = 'customer_id', 'status'
def handle(self):
data = {'customer_id': 'C-1001', 'status': 'confirmed', 'internal_note': 'not for the wire'}
self.response.payload = data
# The response is {"customer_id": "C-1001", "status": "confirmed"}
For responses that are lists, build them through append:
class GetOrders(Service):
output = 'customer_id'
def handle(self):
self.response.payload.append({'customer_id': 'C-1001'})
self.response.payload.append({'customer_id': 'C-1002'})
# The response is [{"customer_id": "C-1001"}, {"customer_id": "C-1002"}]
Model responses
Declaring a data model as output pins the whole shape down - the payload is an instance of the model and only the model's fields can be assigned:
@dataclass(init=False)
class GetCustomerResponse(Model):
name: str
status: str
class GetCustomer(Service):
output = GetCustomerResponse
def handle(self):
self.response.payload.name = 'John Doe'
self.response.payload.status = 'active'
# This would raise an AttributeError - there is no such field in the model
# self.response.payload.address = 'Main Street 123'
A model instance built elsewhere - in a helper method, another module or a mapping layer - can be assigned to the payload as a whole and it serializes from its fields:
class GetCustomer(Service):
output = GetCustomerResponse
def build_response(self):
response = GetCustomerResponse()
response.name = 'John Doe'
response.status = 'active'
return response
def handle(self):
self.response.payload = self.build_response()
Reference
All of the attributes and methods are always available to all services, regardless of the protocol they are invoked through though in the case of HTTP-specific ones, using them will be a no-op if the service is not invoked through HTTP.
| Attribute | Description |
|---|---|
| self.response | The main attribute via which responses are produced |
| self.response.payload | The object to which responses are assigned, i.e. this is the attribute through which a service's business data is returned, such as a JSON message |
| self.response.status_code | (HTTP only) An integer status code such as 200 or 401 to return in response |
| self.response.content_type | (HTTP only) Sets response's Content-Type header value |
| self.response.headers | (HTTP only) A dictionary of header name/value to set in the response |
More information
- Consult the dedicated chapter with programming examples for more details.
- The message building examples show the dot-access pattern in full
- To learn more about data models, click here