idempotency

package
v1.1.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 23, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CanonicalizeJSON

func CanonicalizeJSON(data []byte) ([]byte, error)

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

func ComputeRequestBodyHash(body []byte, params map[string]string) string

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL