mscharhag, Programming and Stuff;

A blog about programming and software development topics, mostly focused on Java technologies including Java EE, Spring and Grails.

Wednesday, 7 October, 2020

REST: Working with asynchronous operations

Sometimes a REST API operation might take a considerable amount of time to complete. Instead of letting the client wait until the operation completes we can return an immediate response and process the request asynchronously. In the following sections we will see how this can be achieved.

Pointing to a status resource

When processing a request asynchronously we should provide a way for the client to get the current processing status. This can be done by introducing a separate status resource. The client can then request this resource in regular intervals until the operation has been completed.

As an example we will use the creation of a product which might take some time to complete.

We start the creation process by issuing this request:

POST /products
{
    "name": "Cool Gadget",
    ...
}

Response:

HTTP/1.1 202 (Accepted)
Location /products/queue/1234

For standard creation requests the server would respond with HTTP 201 and a Location header pointing to the newly created product (See REST Resource creation).

However, in this example, the server responds with HTTP 202 (Accepted). This status code indicates that the request has been accepted but not yet processed. The Location header points to a resource that describes the current processing status.

Clients can obtain the status by sending a GET request to this URI:

GET /products/queue/1234

Response:

HTTP/1.1 200 (OK)

{
    "status": "waiting",
    "issuedAt": "2020-10-03T09:34:24",
    "request": {
        "name": "Cool Gadget",
        ...
    }
    "links": [{ 
        "rel": "cancel", 
        "method": "delete", 
        "href": "/products/queue/1234"
    }]
}

In this example the response contains a status field, an issue date and the request data. Clients can now poll this resource at regular intervals to see if the status changes.

You might also want to provide a way to cancel the operation. This can be done by sending a DELETE request to the status resource:

DELETE /products/queue/1234

If you are using Hypermedia controls in your REST responses you should provide a link to this operation in your response (as shown in the example).

In case you can estimate the time the operation needs you can add a Retry-After HTTP header to the response. This tells the client how long it should wait until sending the follow-up request.

What to do when the operation has been completed?

When the requested operation has been completed we should communicate this through the status resource. If possible the status resource should provide links to processed or newly created resources.

Assume the product from our previous example has been created. If the client now requests the status resource the response looks different:

GET /products/queue/1234

Response:

HTTP/1.1 303 (See other)
Location: /products/321

After the product has been created, the queue element is no longer available. This is indicated by HTTP 303 (See other). The response also contains a Location header that points to the newly created product resource.

However, sending HTTP 303 with a Location header might not always be possible. Assume we have an asynchronous operation that imports multiple products. The result of this operation is not a single resource we can point to.

In this case we should keep the status resource for at least some time. A response might look like this:

{
    "status": "completed",
    "took": "PT5M23S"    
    "imported": [
        {
            "name": "Cool Gadget",
            "links": [{
                "rel": "self", "href": "/products/345"
            }]
        }, {
            "name": "Nice Gadget",
            "links": [{
                "rel": "self", "href": "/products/346"
            }]
        },
        ...
        
    ]
}

The status field indicates that the operation has been completed. The took field contains the processing time as ISO 8601 duration.

If suitable we can provide links to related resources as part of the response.

Callback URLs

Sometimes it can be a viable option to support callbacks. This lets the client submit an url with the request data. When the operation is finished the server sends a request to this url to inform the client. Usually this is a POST request with some status information.

The initial request issued by the client might look like this:

POST /products
{    
    "name": "Cool Gadget",
    ...
    "_callbackUrl": "https://coolapi.myserver.com/product-callback"
}

Here we provide a callback url to the server using the _callbackUrl field. In JSON leading underscore are sometimes used for additional meta properties. Of course you can adapt this to your own style if you don't like this syntax.

As in the previous example the server responds with HTTP 202 and a status resource:

202 Accepted
Location /products/queue/1234

When the operation is finished the server updates the status resource and sends a POST request to the provided URL:

POST https://coolapi.myserver.com/product-callback
{
    "status": "completed",
    "took": "PT14M22S",
    "links": [{ 
        "rel": "product", 
        "href": "/products/321"
    }]
}

This tells the client that the product has been imported successfully.

The problems with callback URLs

Callback handling can increase the complexity on the server side significantly. What happens if a client does not respond on the provided url? How often should it be retried? The server needs to handle all these things.

Authentication can be a problem. If the client and the server are not running in a trusted network the client needs a way to authenticate requests from the server. Otherwise, any untrusted third party would be able to POST data to the callback endpoint. This means the server needs to know its clients because they need to exchange some authentication information before the actual request. The server has to store this information securely for each client.

Callback requests can also be hard to test and debug for developers. Often it is not possible to receive callback requests on a local development machine. This is typically blocked by network policies for security reasons.

Summary

Longer running asynchronous processes can be modeled with REST-APIs. The server tells the client that the request is handled asynchronously by sending a HTTP 202 status code. A Location header is used to point to a resource that gives information about the current processing status. The client can poll this status resource in regular intervals until the operation has been completed. Adding a Retry-After header can reduce unnecessary requests.

Additionally the server can support callback URLs to inform the client after the request has been processed.

 

Leave a reply