Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
133 changes: 133 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,139 @@ 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()
err := next(ctx, cmds)
duration := time.Since(start)

// Log pipeline execution
log.Printf("Pipeline executed %d commands in %v", len(cmds), duration)

// Check for errors in individual commands
for _, cmd := range cmds {
if cmdErr := cmd.Err(); cmdErr != nil {
// Wrap individual command errors if needed
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 the error
wrappedErr := fmt.Errorf("pipeline cmd %s failed: %w", cmd.Name(), cmdErr)
cmd.SetErr(wrappedErr)
}
}

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