Skip to content

Commit d60bbda

Browse files
authored
feat(if-match): add if-match / etag header support (#76)
adding if-match / etag support as a proof of concept that it is possible for grpc APIs to support this behavior, allowing removal of other methods such as etags.
1 parent 04d62f4 commit d60bbda

File tree

6 files changed

+808
-7
lines changed

6 files changed

+808
-7
lines changed

IF_MATCH_DOCUMENTATION.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# If-Match Header Support for Update Operations
2+
3+
This document describes the If-Match header support that has been added to all update endpoints in the AEPC bookstore example.
4+
5+
## Overview
6+
7+
The If-Match header provides optimistic concurrency control for update operations. When provided, the server validates that the current resource matches the expected ETag before performing the update. If the ETags don't match, the update is rejected with a `412 Precondition Failed` status.
8+
9+
## Features
10+
11+
### Supported Operations
12+
13+
The If-Match header is supported for all update operations:
14+
- `UpdateBook`
15+
- `UpdatePublisher`
16+
- `UpdateStore`
17+
- `UpdateItem`
18+
19+
### ETag Generation
20+
21+
ETags are generated by:
22+
1. Serializing the protobuf message using `proto.Marshal`
23+
2. Computing an MD5 hash of the serialized data
24+
3. Encoding the hash as a hexadecimal string
25+
4. Wrapping in quotes (e.g., `"a1b2c3d4..."`)
26+
27+
### Header Processing
28+
29+
The grpc-gateway is configured to forward the `If-Match` HTTP header to gRPC metadata:
30+
- HTTP header: `If-Match: "etag-value"`
31+
- gRPC metadata: `grpcgateway-if-match: "etag-value"`
32+
33+
## Usage
34+
35+
### HTTP API
36+
37+
```bash
38+
# Get a resource to obtain its current state
39+
GET /publishers/1/books/1
40+
41+
# Update with If-Match header
42+
PATCH /publishers/1/books/1
43+
If-Match: "current-etag-value"
44+
Content-Type: application/json
45+
46+
{
47+
"book": {
48+
"price": 30,
49+
"published": true,
50+
"edition": 2
51+
}
52+
}
53+
```
54+
55+
### Response Codes
56+
57+
- `200 OK`: Update successful with valid If-Match header
58+
- `412 Precondition Failed`: If-Match header value doesn't match current resource ETag
59+
- `404 Not Found`: Resource doesn't exist
60+
- No If-Match header: Update proceeds normally (backwards compatible)
61+
62+
### gRPC API
63+
64+
The If-Match header is automatically extracted from gRPC metadata by the service methods. No additional client configuration is required when using grpc-gateway.
65+
66+
## Implementation Details
67+
68+
### Core Components
69+
70+
1. **ETag Generation** (`types.go`):
71+
- `GenerateETag(msg proto.Message)`: Creates ETag from protobuf message
72+
- `ValidateETag(provided, current string)`: Compares ETags with quote handling
73+
74+
2. **Header Extraction** (`service.go`):
75+
- `extractIfMatchHeader(ctx context.Context)`: Extracts If-Match from gRPC metadata
76+
77+
3. **Gateway Configuration** (`gateway.go`):
78+
- Custom header matcher forwards `If-Match` header to `grpcgateway-if-match` metadata
79+
80+
4. **Update Methods**: All update methods now:
81+
- Extract If-Match header from context
82+
- Fetch current resource if header is present
83+
- Generate ETag for current resource
84+
- Validate provided ETag against current ETag
85+
- Reject with `FailedPrecondition` if validation fails
86+
- Proceed with normal update logic if validation passes
87+
88+
### Error Handling
89+
90+
- **Missing Resource**: Returns `NotFound` when trying to validate ETag for non-existent resource
91+
- **ETag Mismatch**: Returns `FailedPrecondition` when If-Match header doesn't match current ETag
92+
- **ETag Generation Failure**: Returns `Internal` if ETag generation fails
93+
- **No If-Match Header**: Proceeds normally for backwards compatibility
94+
95+
## Testing
96+
97+
### Unit Tests
98+
99+
The implementation includes comprehensive unit tests:
100+
- `TestUpdateBookWithIfMatchHeader`: Tests successful and failed ETag validation
101+
- `TestUpdatePublisherWithIfMatchHeader`: Tests publisher-specific ETag handling
102+
- `TestETagGeneration`: Tests ETag generation and validation logic
103+
104+
### Test Coverage
105+
106+
Tests verify:
107+
- ✅ Updates succeed with correct If-Match header
108+
- ✅ Updates fail with incorrect If-Match header (412 status)
109+
- ✅ Updates succeed without If-Match header (backwards compatibility)
110+
- ✅ Updates fail for non-existent resources (404 status)
111+
- ✅ ETag generation produces consistent results for identical content
112+
- ✅ ETag generation produces different results for different content
113+
- ✅ ETag validation handles quoted and unquoted ETags correctly
114+
115+
### Integration Testing
116+
117+
An integration test script is provided (`test_if_match_integration.sh`) that demonstrates:
118+
- End-to-end HTTP API functionality
119+
- Proper error codes for failed preconditions
120+
- Complete workflow from resource creation to ETag-validated updates
121+
122+
## Security Considerations
123+
124+
- ETags are deterministic based on resource content
125+
- ETags do not expose sensitive information (they are content hashes)
126+
- No additional authentication is required beyond existing API security
127+
- ETag validation happens before database operations, preventing unnecessary writes
128+
129+
## Performance Notes
130+
131+
- ETag generation requires serializing and hashing the resource
132+
- Validation requires fetching the current resource before updating
133+
- Impact is minimal for typical update operations
134+
- No additional database operations beyond the standard Get/Update pattern
135+
- ETags are computed on-demand and not stored in the database
136+
137+
## Backwards Compatibility
138+
139+
The If-Match header support is fully backwards compatible:
140+
- Existing clients without If-Match headers continue to work unchanged
141+
- No changes to existing API contracts or response formats
142+
- No new required fields in resources themselves
143+
- All functionality is implemented via HTTP headers and gRPC sidechannel metadata

example/gateway/gateway.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,24 @@ func Run(grpcServerEndpoint string) {
3333
UseProtoNames: true,
3434
},
3535
}),
36+
// Configure header forwarding for If-Match header
37+
runtime.WithIncomingHeaderMatcher(func(key string) (string, bool) {
38+
switch key {
39+
case "If-Match":
40+
return "grpcgateway-if-match", true
41+
default:
42+
return runtime.DefaultHeaderMatcher(key)
43+
}
44+
}),
45+
// Configure outgoing header forwarding for ETag header
46+
runtime.WithOutgoingHeaderMatcher(func(key string) (string, bool) {
47+
switch key {
48+
case "etag":
49+
return "ETag", true
50+
default:
51+
return runtime.DefaultHeaderMatcher(key)
52+
}
53+
}),
3654
)
3755
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}
3856
err := bpb.RegisterBookstoreHandlerFromEndpoint(ctx, mux, grpcServerEndpoint, opts)

0 commit comments

Comments
 (0)