// Package requestid carries a per-request identifier across the gin handler // chain. // // The middleware reads the inbound `X-Request-ID` header, generates a UUIDv4 // when absent, stores the value on the gin context, and reflects it on the // response. Downstream code retrieves the identifier through FromContext. package requestid import ( "context" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // Header is the canonical case-correct header name carrying the request id. const Header = "X-Request-ID" // ginContextKey is the gin.Context key under which the resolved request id is // stored. The value is a string. The key is exported in lowercase form so that // it never collides with handler-level keys; consumers should prefer // FromContext rather than reading the gin context directly. const ginContextKey = "backend.request_id" // requestIDContextKey is the unexported context.Context key used when the // resolved request id is propagated outside gin (background goroutines, // downstream client calls). The unexported value type prevents accidental // collisions across packages. type requestIDContextKey struct{} // Middleware returns the gin middleware that resolves and propagates the // request id. func Middleware() gin.HandlerFunc { return func(c *gin.Context) { requestID := strings.TrimSpace(c.GetHeader(Header)) if requestID == "" { requestID = uuid.NewString() } c.Set(ginContextKey, requestID) c.Writer.Header().Set(Header, requestID) c.Request = c.Request.WithContext(WithValue(c.Request.Context(), requestID)) c.Next() } } // FromContext returns the request id stored on ctx by Middleware. The boolean // reports whether an id was found. Consumers must always check the boolean // before using the returned string. func FromContext(ctx context.Context) (string, bool) { if ctx == nil { return "", false } value, ok := ctx.Value(requestIDContextKey{}).(string) if !ok || value == "" { return "", false } return value, true } // FromGin returns the request id stored on the gin context by Middleware. The // boolean reports whether an id was found. func FromGin(c *gin.Context) (string, bool) { if c == nil { return "", false } raw, ok := c.Get(ginContextKey) if !ok { return "", false } value, ok := raw.(string) if !ok || value == "" { return "", false } return value, true } // WithValue stores requestID on ctx under the package-private context key. // Exposed primarily for tests that build a context outside the middleware. func WithValue(ctx context.Context, requestID string) context.Context { return context.WithValue(ctx, requestIDContextKey{}, requestID) }