Two clients read the same offer, both change it, and the second save silently overwrites the first. ETags prevent that. ORDS sends an ETag header with each resource, a fingerprint of its current state. A client that sends the ETag back in If-Match on a PUT gets 412 Precondition Failed if someone else changed the resource in between. If-None-Match on a GET saves bandwidth with 304 Not Modified.
Before You Start
You need ORDS installed and running against your database, and a schema to work in. The examples use NIMBUS, the sample schema of a fictional airline, which you can install with the scripts in the setup/nimbus folder of the Oracle Database 26ai code repository on GitHub. The schema comes from Oracle Database 26ai SQL and PL/SQL Book.
ORDS in the examples answers at https://localhost:8443/ords/, and NIMBUS is REST-enabled with the URL alias nimbus. Replace the host and port with your own ORDS address. The curl commands use -k because the test server has a self-signed certificate; leave it out when your certificate is trusted. JSON responses are formatted for reading; ORDS returns them on one line.
The examples use the offers table published with AutoREST in How to REST-Enable a Table with ORDS AutoREST, and the lounge module from How to Write a POST Handler That Inserts Rows in ORDS.
Syntax
GET ... -> response header ETag: "value"
GET ... with If-None-Match: "value" -> 304 when unchanged
PUT ... with If-Match: "value" -> 412 when changed since
ords.define_template(..., p_etag_type => 'HASH'); -- default: hash of the response
ords.define_template(..., p_etag_type => 'QUERY',
p_etag_query => 'select ... where id = :id');Read the ETag
Example:
curl -k -D - -o /dev/null https://localhost:8443/ords/nimbus/offers/1
Output (status and ETag):
HTTP/1.1 200 OK ETag: "LYWBGhOJ14IIAFV6DhAhzw9o5purS6fON+ia/Zrpg0I+AP7haFrecLbpC9n+juftgf2tcK8kkhR9hoNM5M0emQ=="
Ask Only for Changes
Example:
curl -k -D - -o /dev/null -H 'If-None-Match: "ETAG_FROM_THE_GET"' \ https://localhost:8443/ords/nimbus/offers/1
Output:
HTTP/1.1 304 Not Modified ETag: "LYWBGhOJ14IIAFV6DhAhzw9o5purS6fON+ia/Zrpg0I+AP7haFrecLbpC9n+juftgf2tcK8kkhR9hoNM5M0emQ=="
The offer has not changed, so ORDS answers 304 with no body.
Update Only the Version You Read
A PUT with the current ETag in If-Match succeeds and returns a new ETag:
Output:
HTTP/1.1 200 OK
ETag: "adwPQMCn2BumCu5oLxb49O43DTqwsMbbOlQezn5wv1KqEuyOS+5IUFilLgmyH7/EoMxpdvZzb4ElkjuPJAWaQQ=="
{"offer_id":1,"airport_code":"DXB","title":"Day pass, Terminal 3 lounge","price_usd":62,"valid_until":"2026-12-31T00:00:00Z","links":[{"rel":"self","href":"https://localhost:8443/ords/nimbus/offers/1"},{"rel":"edit","href":"https://localhost:8443/ords/nimbus/offers/1"},{"rel":"describedby","href":"https://localhost:8443/ords/nimbus/metadata-catalog/offers/item"},{"rel":"collection","href":"https://localhost:8443/ords/nimbus/offers/"}]}A second PUT that still sends the first ETag is refused, because the offer has changed since that version:
Output:
HTTP/1.1 412 Precondition Failed
Content-Type: application/problem+json
Content-Length: 204
{
"code": "PredconditionFailed",
"message": "Predcondition Failed",
"type": "tag:oracle.com,2020:error/PredconditionFailed",
"instance": "tag:oracle.com,2020:ecid/NYaYGyZwBz1STOGKrlP9jQ"
}The spelling PredconditionFailed is ORDS's own. The client should read the offer again, apply its change to the new version, and retry.
ETags for REST Module Templates
Templates use a hash of the response by default. With p_etag_type set to QUERY, ORDS computes the ETag from a query of your own, for example over the columns that matter:
Example:
begin
ords.define_template(p_module_name => 'lounge',
p_pattern => 'offers/:id',
p_etag_type => 'QUERY',
p_etag_query => 'select ora_hash(title || price_usd || valid_until)
from lounge_offers where offer_id = :id');
commit;
end;
/
select uri_template, etag_type, etag_query from user_ords_templates where uri_template = 'offers/:id';Output:
PL/SQL procedure successfully completed.
URI_TEMPLATE ETAG_TYPE ETAG_QUERY
_______________ ____________ _____________________________________________________________________________________
offers/:id QUERY select ora_hash(title || price_usd || valid_until)
from lounge_offers where offer_id = :idORDS.DEFINE_TEMPLATE replaces the template, and in the test that removed its GET, PUT, and DELETE handlers: the endpoint answered 404 until they were defined again.
Example:
-- DEFINE_TEMPLATE replaced the template, so its handlers have to be defined again
begin
ords.define_handler(p_module_name => 'lounge',
p_pattern => 'offers/:id',
p_method => 'GET',
p_source_type => ords.source_type_collection_item,
p_source => 'select offer_id, airport_code, title, price_usd, valid_until
from lounge_offers
where offer_id = :id');
commit;
end;
/The template then returns an ETag, and a GET with that value in If-None-Match gets 304:
Output:
HTTP/1.1 200 OK ETag: "CJZfWQdEJzntRLQj6mfmiK44eTw1fUYeqJI1gPn4C2eAAe9ujcKXes15PvT6EwliYqvGE0cZuzewVBXWXJILqg==" HTTP/1.1 304 Not Modified
Things to Know
- ETags are quoted strings: send them back exactly as received, quotes included.
- A QUERY ETag is cheaper than hashing a large response, and can ignore columns that do not matter to clients.
- If-Match is optional for clients; only those that send it are protected against lost updates.
Related Guides
Conclusion
ORDS ETags give REST clients optimistic locking and cheap revalidation: send If-Match on updates to get 412 instead of a lost update, and If-None-Match on reads to get 304 when nothing changed. For module templates, choose HASH or a QUERY of your own, and define handlers again after redefining a template.
