HATEOAS Conditional Requests — Using ETags and If-None-Match in Hypermedia APIs
In this tutorial, you will learn about HATEOAS Conditional Requests. We cover key concepts, practical examples, and best practices to help you master this topic.
HATEOAS conditional requests use ETags and If-None-Match headers to cache hypermedia responses, reducing bandwidth by only returning full responses when links or resource state changes.
What You'll Learn
- ETag generation strategies for hypermedia resources
- Conditional GET with If-None-Match for Caching
- Optimistic concurrency with If-Match and If-Unmodified-Since
- Cache invalidation when links change
- Combining HATEOAS with HTTP caching headers
Why It Matters
Hypermedia responses often contain many links that are expensive to generate and transmit. Conditional requests reduce bandwidth by 60-80% for read-heavy APIs. DodaTech's hypermedia threat analysis API uses conditional requests to serve thousands of concurrent security scanners without overwhelming the database.
Real-World Use
A threat intelligence API returns hypermedia responses with links to related threat reports, analysis actions, and remediation guides. With conditional requests, scanners re-fetch only when new threats appear, reducing bandwidth by 75%.
sequenceDiagram
Client->>Server: GET /threats/123 (If-None-Match: "abc123")
Server->>Server: Check ETag match
alt ETag matches
Server-->>Client: 304 Not Modified (empty body)
else ETag changed
Server-->>Client: 200 OK + ETag: "def456" + Hypermedia body
end
Client->>Client: Updates cached representation
Code Examples
Example 1: ETag Generation from Link State
import hashlib
import json
def generate_etag(resource_id, links, updated_at):
"""Generate ETag based on resource data and links."""
data = {
'id': resource_id,
'links': sorted(links, key=lambda l: l['rel']),
'updated_at': updated_at.isoformat()
}
content = json.dumps(data, sort_keys=True).encode()
return hashlib.sha256(content).hexdigest()
links = [
{'rel': 'self', 'href': '/threats/123'},
{'rel': 'analysis', 'href': '/threats/123/analysis'},
{'rel': 'remediation', 'href': '/threats/123/fix'}
]
etag = generate_etag('123', links, datetime.now())
print(f"ETag: {etag}")
# Output: ETag: a1b2c3d4e5f6...
Example 2: Conditional GET Handler
from flask import Flask, request, jsonify, make_response
app = Flask(__name__)
@app.route('/threats/<threat_id>')
def get_threat(threat_id):
threat = find_threat(threat_id)
hypermedia = build_hypermedia(threat)
body = json.dumps(hypermedia)
current_etag = hashlib.md5(body.encode()).hexdigest()
# Check If-None-Match
client_etag = request.headers.get('If-None-Match', '').strip('"')
if client_etag == current_etag:
return make_response('', 304)
response = make_response(hypermedia, 200)
response.headers['ETag'] = f'"{current_etag}"'
response.headers['Cache-Control'] = 'no-cache'
return response
Example 3: Optimistic Concurrency with Link Updates
@app.route('/threats/<threat_id>/analyze', methods=['POST'])
def analyze_threat(threat_id):
client_etag = request.headers.get('If-Match', '').strip('"')
threat = find_threat(threat_id)
current_etag = compute_etag(threat)
if client_etag != current_etag:
return jsonify({
'error': 'Resource changed since last fetch',
'status': 412,
'links': [
{'rel': 'self', 'href': f'/threats/{threat_id}'},
{'rel': 'latest', 'href': f'/threats/{threat_id}?refresh=true'}
]
}), 412
# Proceed with analysis
result = run_analysis(threat)
new_links = [
{'rel': 'self', 'href': f'/threats/{threat_id}'},
{'rel': 'results', 'href': f'/threats/{threat_id}/results/{result.id}'}
]
response = jsonify({'status': 'analyzing', '_links': new_links})
response.headers['ETag'] = f'"{generate_etag(threat_id, new_links, datetime.now())}"'
return response
Common Mistakes
1. Using Weak ETags for Hypermedia
Weak ETags allow semantically equivalent but byte-different responses. Hypermedia changes must use strong ETags.
2. Only Caching Response Bodies
Cache link structures separately from data — links change independently of resource state.
3. Ignoring Vary Header
Always set Vary: Accept, Authorization so different users get different hypermedia links.
4. Expiring Caches Too Aggressively
Use ETag validation instead of short max-age to avoid 304 round-trips on every request.
5. Not Invalidating Caches on Link Changes
When links change (e.g., user permissions), the ETag must change even if resource data is identical.
Practice Questions
- What is the difference between strong and weak ETags for hypermedia?
- How does If-None-Match reduce bandwidth?
- Why is the Vary header important for hypermedia caching?
- When should you use If-Match instead of If-None-Match?
- How do you handle cache invalidation when user permissions change?
Answers:
- Strong ETags change when any byte changes; weak (~) allow semantic equivalence. Hypermedia uses strong ETags.
- It returns 304 Not Modified when the client's cached version is current, saving the full response body.
- Different clients may see different links based on Authorization or Accept headers.
- If-Match is for write operations (optimistic concurrency); If-None-Match is for read caching.
- Regenerate the ETag whenever user permissions change, even if resource data stays the same.
Challenge: Build a middleware that generates ETags for hypermedia responses by hashing both the resource data and link list. Verify that changing user roles produces a different ETag.
FAQ
What's Next
Combine conditional requests with HATEOAS Caching for a complete caching Strategy, then explore HATEOAS Content Negotiation for serving different hypermedia formats.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro