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']
curl -X POST http://localhost:11223/api/orders \
  -d '{"customer_id":"C-1001", "quantity":3}'

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:

AttributeError: No such key `city` among `["customer_id", "quantity"]`

To read a key that may or may not be there, check for it first:

def handle(self):
    if 'priority' in self.request.input:
        priority = self.request.input.priority

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

Try it: Run the first curl call above against your own channel, then remove the quantity field from the JSON and read it in the service anyway - what error do you get back and which line does it point to?

Declared input

Input can also be declared up front. A declaration means that:

  • The declared names are parsed into input before handle runs
  • 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
curl "http://localhost:11223/api/profile?customer_id=C-1001"

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

Try it: Invoke GetProfile without the customer_id parameter - what does the caller receive and what does the server log say?

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:

def handle(self):
    region = self.request.http.GET.region
curl "http://localhost:11223/api/orders?region=north"

Read a path parameter - for a channel whose URL path is /api/orders/{order_id}:

def handle(self):
    order_id = self.request.http.params.order_id
curl http://localhost:11223/api/orders/ORD-123

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:

def handle(self):
    api_key = self.request.http.headers['x-api-key']
curl -H "X-API-Key: abc" http://localhost:11223/api/orders

Read form data from a multipart request:

def handle(self):
    form = self.request.http.get_form_data()
    file_name = form['file_name']

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:

AttributeDescription
self.requestAlong with self.channel, one of the main attributes describing incoming messages
self.request.inputThe incoming message, parsed - see Reading input and Declared input
self.request.rawThe message exactly as received, str or bytes - see Raw requests
self.request.cidCorrelation ID for the current request - useful for logging and tracing
self.request.data_formatData format of the request, e.g. 'json' or 'xml'
self.request.transportTransport type used for the request
self.channelAlong 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.idUnique ID of the channel
self.channel.nameName of the channel the request was received through
self.channel.typeType of the channel - will be equal to one of the constants in zato.common.CHANNEL
self.chanAlias to self.channel
self.channel.securityDescribes a security definition attached to the channel, if any is at all
self.channel.security.nameName of the security definition
self.channel.security.usernameUsername used to invoke the channel, if applicable for a particular security type
self.channel.security.typeType of the security definition - will be equal to one of the constants in zato.common.SEC_DEF_TYPE
self.channel.secAlias to self.channel.security

HTTP-specific attributes, each shown in an example in Channel context - HTTP:

AttributeDescription
self.request.httpThe attribute to use to access HTTP-specific information
self.request.http.methodHTTP method used to invoke the service
self.request.http.GETAll 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.POSTAll 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.pathURL 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.paramsA 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.headersAll HTTP headers as a Bunch object, names lower-cased and dash-joined, e.g. 'x-api-key'
self.request.http.user_agentThe User-Agent header from the HTTP request
self.request.http.get_form_data()Returns form data from multipart requests as a dictionary
self.wsgi_environWhile 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

Overview

All services produce responses through self.response.payload. What you assign decides what goes to the wire:

  1. Dot access on the payload builds a nested structure of any depth and it serializes to the channel's data format
  2. A dict replaces anything built so far and serializes as it is
  3. A list of dicts becomes an array on the wire
  4. A string passes through to the wire exactly as it is
  5. A model instance serializes from its declared fields
  6. 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'
{"customer": {"name": "John Doe", "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},
    ]
{"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:

def handle(self):
    self.response.payload = {'id':123, 'name':'John Doe'}

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:

def handle(self):
    self.response.payload = '{"id":123, "name":"John Doe"}'

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.

Try it: Build the nested customer response above in a service of your own, then add one more assignment below the existing two - does the order of keys in the JSON output match the order of your assignments?

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()

Try it: Uncomment the address line in the GetCustomer example - which line does the error point to and what does it say?

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.

AttributeDescription
self.responseThe main attribute via which responses are produced
self.response.payloadThe 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