When dealing with real-time updates, some Twilio API resources utilize the If-Match
and ETag
HTTP headers for conditional mutation. Every response containing these objects will return an ETag
value, which you can compare against an If-Match
header.
The following resources currently support this form of conflict resolution:
Let's walk through a workflow using the Task resource:
We often make requests with the idea of "optimistic concurrency". For example, let's say that you request to create a Task, like so:
1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID and Auth Token at twilio.com/console5// and set the environment variables. See http://twil.io/secure6const accountSid = process.env.TWILIO_ACCOUNT_SID;7const authToken = process.env.TWILIO_AUTH_TOKEN;8const client = twilio(accountSid, authToken);910async function createTask() {11const task = await client.taskrouter.v112.workspaces("WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")13.tasks.create({ attributes: JSON.stringify({ foo: "bar" }) });1415console.log(task.accountSid);16}1718createTask();
1{2"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",3"age": 25200,4"assignment_status": "pending",5"attributes": "{\"foo\": \"bar\"}",6"date_created": "2014-05-14T18:50:02Z",7"date_updated": "2014-05-15T07:26:06Z",8"task_queue_entered_date": null,9"virtual_start_time": "2014-05-14T18:50:02Z",10"priority": 1,11"reason": "Test Reason",12"sid": "WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",13"task_queue_sid": "WQaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",14"task_channel_sid": "TCaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",15"task_channel_unique_name": "unique",16"timeout": 60,17"url": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Tasks/WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",18"workspace_sid": "WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",19"workflow_sid": "WWaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",20"workflow_friendly_name": "Example Workflow",21"task_queue_friendly_name": "Example Task Queue",22"ignore_capacity": false,23"routing_target": "WKaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",24"addons": "{}",25"required_attention": 0,26"links": {27"task_queue": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/TaskQueues/WQaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",28"workflow": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Workflows/WWaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",29"workspace": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",30"reservations": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Tasks/WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Reservations"31}32}
This will create a Task with the attributes {"foo": "bar"}. Now you can update that object through the REST API.
1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID and Auth Token at twilio.com/console5// and set the environment variables. See http://twil.io/secure6const accountSid = process.env.TWILIO_ACCOUNT_SID;7const authToken = process.env.TWILIO_AUTH_TOKEN;8const client = twilio(accountSid, authToken);910async function updateTask() {11const task = await client.taskrouter.v112.workspaces("WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")13.tasks("WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")14.update({ attributes: JSON.stringify({ foo: "bar-SFO" }) });1516console.log(task.accountSid);17}1819updateTask();
1{2"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",3"age": 25200,4"assignment_status": "pending",5"attributes": "{\"foo\": \"bar-SFO\"}",6"date_created": "2014-05-14T18:50:02Z",7"date_updated": "2014-05-15T07:26:06Z",8"task_queue_entered_date": "2014-05-14T18:50:02Z",9"virtual_start_time": "2023-08-02T12:34:56Z",10"priority": 0,11"reason": "Test Reason",12"sid": "WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",13"task_queue_sid": "WQaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",14"task_channel_sid": "TCaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",15"task_channel_unique_name": "task-channel",16"timeout": 60,17"url": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Tasks/WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",18"workflow_sid": "WWaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",19"workspace_sid": "WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",20"workflow_friendly_name": "Test Workflow",21"task_queue_friendly_name": "Test Queue",22"addons": "{}",23"ignore_capacity": false,24"routing_target": "WKaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",25"required_attention": 0,26"links": {27"task_queue": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/TaskQueues/WQaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",28"workflow": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Workflows/WWaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",29"workspace": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",30"reservations": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Tasks/WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Reservations"31}32}
You'll get a response back, and the attributes will now be {"foo": "bar-SFO"}
. This is optimistic concurrency - you assume that you are the only person making changes to the Task, so foo
used to be bar
, and now it's bar-SFO
.
This might work a lot of the time, but let's say there is an argument over whether the attribute should be bar-SFO
or bar-DEN
. Now you have two POST
requests trying to update the Task in quick succession: POST A
(the same request as above) and POST B
.
What will happen in this condition?
1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID and Auth Token at twilio.com/console5// and set the environment variables. See http://twil.io/secure6const accountSid = process.env.TWILIO_ACCOUNT_SID;7const authToken = process.env.TWILIO_AUTH_TOKEN;8const client = twilio(accountSid, authToken);910async function updateTask() {11const task = await client.taskrouter.v112.workspaces("WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")13.tasks("WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")14.update({ attributes: JSON.stringify({ foo: "bar-DEN" }) });1516console.log(task.accountSid);17}1819updateTask();
1{2"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",3"age": 25200,4"assignment_status": "pending",5"attributes": "{\"foo\": \"bar-DEN\"}",6"date_created": "2014-05-14T18:50:02Z",7"date_updated": "2014-05-15T07:26:06Z",8"task_queue_entered_date": "2014-05-14T18:50:02Z",9"virtual_start_time": "2023-08-02T12:34:56Z",10"priority": 0,11"reason": "Test Reason",12"sid": "WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",13"task_queue_sid": "WQaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",14"task_channel_sid": "TCaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",15"task_channel_unique_name": "task-channel",16"timeout": 60,17"url": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Tasks/WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",18"workflow_sid": "WWaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",19"workspace_sid": "WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",20"workflow_friendly_name": "Test Workflow",21"task_queue_friendly_name": "Test Queue",22"addons": "{}",23"ignore_capacity": false,24"routing_target": "WKaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",25"required_attention": 0,26"links": {27"task_queue": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/TaskQueues/WQaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",28"workflow": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Workflows/WWaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",29"workspace": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",30"reservations": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Tasks/WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Reservations"31}32}
The last writer wins. Because we're operating on the model of optimistic concurrency, POST
B will simply overwrite POST
A.
Fortunately, you can avoid these race conditions. You'll notice that the attributes aren't the only thing that changed when you updated your resource: Your ETag header will have incremented from 0 to 1 in the response.
Now, you can use the If-Match
header to check that the version of the Resource is up-to-date before sending your update through. First, GET
the Task to retrieve the ETag
header. Then pass this revision as a value in the If-Match
header when you post your update. This ensures that, if you are not referencing the most up-to-date revision, the update will be rejected.
1// Download the helper library from https://www.twilio.com/docs/node/install2const twilio = require("twilio"); // Or, for ESM: import twilio from "twilio";34// Find your Account SID and Auth Token at twilio.com/console5// and set the environment variables. See http://twil.io/secure6const accountSid = process.env.TWILIO_ACCOUNT_SID;7const authToken = process.env.TWILIO_AUTH_TOKEN;8const client = twilio(accountSid, authToken);910async function updateTask() {11const task = await client.taskrouter.v112.workspaces("WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")13.tasks("WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")14.update({15attributes: JSON.stringify({ foo: "bar-DEN" }),16ifMatch: "0",17});1819console.log(task.accountSid);20}2122updateTask();
1{2"account_sid": "ACaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",3"age": 25200,4"assignment_status": "pending",5"attributes": "{\"foo\": \"bar-DEN\"}",6"date_created": "2014-05-14T18:50:02Z",7"date_updated": "2014-05-15T07:26:06Z",8"task_queue_entered_date": "2014-05-14T18:50:02Z",9"virtual_start_time": "2023-08-02T12:34:56Z",10"priority": 0,11"reason": "Test Reason",12"sid": "WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",13"task_queue_sid": "WQaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",14"task_channel_sid": "TCaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",15"task_channel_unique_name": "task-channel",16"timeout": 60,17"url": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Tasks/WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",18"workflow_sid": "WWaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",19"workspace_sid": "WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",20"workflow_friendly_name": "Test Workflow",21"task_queue_friendly_name": "Test Queue",22"addons": "{}",23"ignore_capacity": false,24"routing_target": "WKaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",25"required_attention": 0,26"links": {27"task_queue": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/TaskQueues/WQaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",28"workflow": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Workflows/WWaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",29"workspace": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",30"reservations": "https://taskrouter.twilio.com/v1/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Tasks/WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Reservations"31}32}
Note the addition of the If-Match
header. Now, this operation will fail with a 412
Response because the submitted ETag
value is 0 and the current Attributes are different.
When you're testing or building a simple application, this might not be a critical step. If, however, your resource can be mutated by multiple sources at a time, using these headers will help ensure that you don't accidentally overwrite and/or lose any updates.