Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,144 @@ vals, err := rdb.Eval(ctx, "return {KEYS[1],ARGV[1]}", []string{"key"}, "hello")
res, err := rdb.Do(ctx, "set", "key", "value").Result()
```

## Typed Errors

go-redis provides typed error checking functions for common Redis errors:

```go
// Cluster and replication errors
redis.IsLoadingError(err) // Redis is loading the dataset
redis.IsReadOnlyError(err) // Write to read-only replica
redis.IsClusterDownError(err) // Cluster is down
redis.IsTryAgainError(err) // Command should be retried
redis.IsMasterDownError(err) // Master is down
redis.IsMovedError(err) // Returns (address, true) if key moved
redis.IsAskError(err) // Returns (address, true) if key being migrated

// Connection and resource errors
redis.IsMaxClientsError(err) // Maximum clients reached
redis.IsAuthError(err) // Authentication failed (NOAUTH, WRONGPASS, unauthenticated)
redis.IsPermissionError(err) // Permission denied (NOPERM)
redis.IsOOMError(err) // Out of memory (OOM)

// Transaction errors
redis.IsExecAbortError(err) // Transaction aborted (EXECABORT)
```

### Error Wrapping in Hooks

When wrapping errors in hooks, use custom error types with `Unwrap()` method (preferred) or `fmt.Errorf` with `%w`. Always call `cmd.SetErr()` to preserve error type information:

```go
// Custom error type (preferred)
type AppError struct {
Code string
RequestID string
Err error
}

func (e *AppError) Error() string {
return fmt.Sprintf("[%s] request_id=%s: %v", e.Code, e.RequestID, e.Err)
}

func (e *AppError) Unwrap() error {
return e.Err
}

// Hook implementation
func (h MyHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
return func(ctx context.Context, cmd redis.Cmder) error {
err := next(ctx, cmd)
if err != nil {
// Wrap with custom error type
wrappedErr := &AppError{
Code: "REDIS_ERROR",
RequestID: getRequestID(ctx),
Err: err,
}
cmd.SetErr(wrappedErr)
return wrappedErr // Return wrapped error to preserve it
}
return nil
}
}

// Typed error detection works through wrappers
if redis.IsLoadingError(err) {
// Retry logic
}

// Extract custom error if needed
var appErr *AppError
if errors.As(err, &appErr) {
log.Printf("Request: %s", appErr.RequestID)
}
```

Alternatively, use `fmt.Errorf` with `%w`:
```go
wrappedErr := fmt.Errorf("context: %w", err)
cmd.SetErr(wrappedErr)
```

### Pipeline Hook Example

For pipeline operations, use `ProcessPipelineHook`:

```go
type PipelineLoggingHook struct{}

func (h PipelineLoggingHook) DialHook(next redis.DialHook) redis.DialHook {
return next
}

func (h PipelineLoggingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
return next
}

func (h PipelineLoggingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
return func(ctx context.Context, cmds []redis.Cmder) error {
start := time.Now()

// Execute the pipeline
err := next(ctx, cmds)

duration := time.Since(start)
log.Printf("Pipeline executed %d commands in %v", len(cmds), duration)

// Process individual command errors
// Note: Individual command errors are already set on each cmd by the pipeline execution
for _, cmd := range cmds {
if cmdErr := cmd.Err(); cmdErr != nil {
// Check for specific error types using typed error functions
if redis.IsAuthError(cmdErr) {
log.Printf("Auth error in pipeline command %s: %v", cmd.Name(), cmdErr)
} else if redis.IsPermissionError(cmdErr) {
log.Printf("Permission error in pipeline command %s: %v", cmd.Name(), cmdErr)
}

// Optionally wrap individual command errors to add context
// The wrapped error preserves type information through errors.As()
wrappedErr := fmt.Errorf("pipeline cmd %s failed: %w", cmd.Name(), cmdErr)
cmd.SetErr(wrappedErr)
}
}

// Return the pipeline-level error (connection errors, etc.)
// You can wrap it if needed, or return it as-is
return err
}
}

// Register the hook
rdb.AddHook(PipelineLoggingHook{})

// Use pipeline - errors are still properly typed
pipe := rdb.Pipeline()
pipe.Set(ctx, "key1", "value1", 0)
pipe.Get(ctx, "key2")
_, err := pipe.Exec(ctx)
```

## Run the test

Expand Down
8 changes: 4 additions & 4 deletions acl_commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ var _ = Describe("ACL permissions", Label("NonRedisEnterprise"), func() {

// no perm for dropindex
err = c.FTDropIndex(ctx, "txt").Err()
Expect(err).ToNot(BeEmpty())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("NOPERM"))

// json set and get have perm
Expand All @@ -315,15 +315,15 @@ var _ = Describe("ACL permissions", Label("NonRedisEnterprise"), func() {

// no perm for json clear
err = c.JSONClear(ctx, "foo", "$").Err()
Expect(err).ToNot(BeEmpty())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("NOPERM"))

// perm for reserve
Expect(c.BFReserve(ctx, "bloom", 0.01, 100).Err()).NotTo(HaveOccurred())

// no perm for info
err = c.BFInfo(ctx, "bloom").Err()
Expect(err).ToNot(BeEmpty())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("NOPERM"))

// perm for cf.reserve
Expand All @@ -338,7 +338,7 @@ var _ = Describe("ACL permissions", Label("NonRedisEnterprise"), func() {
Expect(c.TSCreate(ctx, "tsts").Err()).NotTo(HaveOccurred())
// noperm for ts.info
err = c.TSInfo(ctx, "tsts").Err()
Expect(err).ToNot(BeEmpty())
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("NOPERM"))

Expect(client.FlushDB(ctx).Err()).NotTo(HaveOccurred())
Expand Down
Loading
Loading