Documentation
¶
Index ¶
- func CanonicalizeJSON(data []byte) ([]byte, error)
- func ComputeHTTPScopeHash(actorID, targetAccountID *string, ...) string
- func ComputeRequestBodyHash(body []byte, params map[string]string) string
- func ComputeServiceScopeHash(actorID, targetAccountID *string, service, handler, idempotencyKey string) string
- type CachedResult
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func CanonicalizeJSON ¶
CanonicalizeJSON re-encodes a JSON value into a deterministic byte representation. It round-trips the data through json.Unmarshal/json.Marshal, which normalizes whitespace and (via Go's map iteration + encoding/json's sorted-key output) produces consistent key ordering for objects at all nesting levels.
Array element order is preserved — only object key order is normalized.
Returns ([]byte{}, nil) for empty input and (nil, error) for malformed JSON.
func ComputeHTTPScopeHash ¶
func ComputeHTTPScopeHash(actorID, targetAccountID *string, method, normalizedRoute, idempotencyKey string) string
ComputeHTTPScopeHash produces a deterministic SHA-256 hex digest that uniquely identifies the "scope" of an idempotent HTTP request. The scope binds the idempotency key to a specific actor, target account, HTTP method, and route so that the same key used by a different actor or against a different endpoint is treated as a distinct request.
Fields are concatenated with a unit-separator delimiter and hashed:
SHA256(actorID + \x1f + targetAccountID + \x1f + method + \x1f + normalizedRoute + \x1f + idempotencyKey)
Nil pointer fields (actorID, targetAccountID) are coerced to empty strings, meaning nil and "" produce the same hash. This is intentional — unauthenticated requests have no actor ID, and some endpoints have no target account.
The returned hash is stored in the idempotency_keys table as scope_hash and compared on subsequent requests to detect key reuse across different scopes.
func ComputeRequestBodyHash ¶
ComputeRequestBodyHash produces a deterministic SHA-256 hex digest of the request payload. It is stored alongside the scope hash and compared on retries to detect when a client reuses the same idempotency key with a different request body (which is an error — the key should be unique per intended mutation).
The body is first canonicalized via CanonicalizeJSON so that semantically identical payloads with different key ordering or whitespace produce the same hash. If the body is not valid JSON (e.g. form-encoded), the raw bytes are used as-is.
URL query parameters are appended to the canonical body in sorted key order, separated by the unit-separator character. This ensures that requests like POST /api?page=1&limit=10 and POST /api?limit=10&page=1 hash identically.
func ComputeServiceScopeHash ¶
func ComputeServiceScopeHash(actorID, targetAccountID *string, service, handler, idempotencyKey string) string
ComputeServiceScopeHash produces a deterministic SHA-256 hex digest that uniquely identifies the "scope" of an idempotent gRPC service call. This is the service-layer counterpart of ComputeHTTPScopeHash, used by idempotency mediators in backend services (e.g. auth-service) rather than the HTTP gateway.
Fields are concatenated with a unit-separator delimiter and hashed:
SHA256(actorID + \x1f + targetAccountID + \x1f + service + \x1f + handler + \x1f + idempotencyKey)
Nil pointer fields (actorID, targetAccountID) are coerced to empty strings, meaning nil and "" produce the same hash. This is intentional — unauthenticated requests have no actor ID, and some endpoints have no target account.
Types ¶
type CachedResult ¶
type CachedResult[T any] struct { // HasCache is true when a previously stored response was found for the idempotency key. When false, the handler should proceed normally (first-time request). HasCache bool // Data holds the deserialized success response body. Non-nil only when HasCache is true and the original response had a status code below 400. Data *T // Error holds the deserialized API error. Non-nil only when HasCache is true and the original response had a status code >= 400. Error *apierror.APIError }
CachedResult holds the outcome of attempting to deserialize a previously stored idempotency response. Service handlers use this to short-circuit execution when a cache hit is found.
The type parameter T is the expected success response struct (e.g. a proto-generated message or a domain DTO). On a cache hit the result contains either the deserialized success data or a structured API error, depending on the original response's status code.
func UnmarshalCachedResponse ¶
func UnmarshalCachedResponse[T any](ctx context.Context, statusCode *int, body json.RawMessage) (CachedResult[T], error)
UnmarshalCachedResponse deserializes a stored idempotency response into a typed CachedResult. It is called by service handlers after looking up an idempotency key to determine whether a cached response can be returned instead of re-executing the handler logic.
Behavior by input:
- statusCode == nil: no cache entry exists; returns {HasCache: false}.
- statusCode >= 400: the original request produced an error; body is deserialized as an APIError via apierror.APIErrorFromJSON.
- statusCode < 400: the original request succeeded; body is deserialized into T.
In both cache-hit cases, appctx.MarkIdempotencyReplayed is called so the transport layer can set the appropriate idempotent-replayed header on the outgoing response.
Returns an error if body is empty (when statusCode is non-nil) or if JSON deserialization fails. These are internal errors that indicate a corrupted cache entry rather than a client mistake.