|
| 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 |
0 commit comments