Skip to content

Commit 3b23fce

Browse files
committed
add common error types
1 parent c4dc1ee commit 3b23fce

File tree

5 files changed

+404
-10
lines changed

5 files changed

+404
-10
lines changed

README.md

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,14 +434,23 @@ res, err := rdb.Do(ctx, "set", "key", "value").Result()
434434
go-redis provides typed error checking functions for common Redis errors:
435435
436436
```go
437+
// Cluster and replication errors
437438
redis.IsLoadingError(err) // Redis is loading the dataset
438439
redis.IsReadOnlyError(err) // Write to read-only replica
439440
redis.IsClusterDownError(err) // Cluster is down
440441
redis.IsTryAgainError(err) // Command should be retried
441442
redis.IsMasterDownError(err) // Master is down
442-
redis.IsMaxClientsError(err) // Maximum clients reached
443443
redis.IsMovedError(err) // Returns (address, true) if key moved
444444
redis.IsAskError(err) // Returns (address, true) if key being migrated
445+
446+
// Connection and resource errors
447+
redis.IsMaxClientsError(err) // Maximum clients reached
448+
redis.IsAuthError(err) // Authentication failed (NOAUTH, WRONGPASS, unauthenticated)
449+
redis.IsPermissionError(err) // Permission denied (NOPERM)
450+
redis.IsOOMError(err) // Out of memory (OOM)
451+
452+
// Transaction errors
453+
redis.IsExecAbortError(err) // Transaction aborted (EXECABORT)
445454
```
446455
447456
### Error Wrapping in Hooks
@@ -500,6 +509,60 @@ wrappedErr := fmt.Errorf("context: %w", err)
500509
cmd.SetErr(wrappedErr)
501510
```
502511
512+
### Pipeline Hook Example
513+
514+
For pipeline operations, use `ProcessPipelineHook`:
515+
516+
```go
517+
type PipelineLoggingHook struct{}
518+
519+
func (h PipelineLoggingHook) DialHook(next redis.DialHook) redis.DialHook {
520+
return next
521+
}
522+
523+
func (h PipelineLoggingHook) ProcessHook(next redis.ProcessHook) redis.ProcessHook {
524+
return next
525+
}
526+
527+
func (h PipelineLoggingHook) ProcessPipelineHook(next redis.ProcessPipelineHook) redis.ProcessPipelineHook {
528+
return func(ctx context.Context, cmds []redis.Cmder) error {
529+
start := time.Now()
530+
err := next(ctx, cmds)
531+
duration := time.Since(start)
532+
533+
// Log pipeline execution
534+
log.Printf("Pipeline executed %d commands in %v", len(cmds), duration)
535+
536+
// Check for errors in individual commands
537+
for _, cmd := range cmds {
538+
if cmdErr := cmd.Err(); cmdErr != nil {
539+
// Wrap individual command errors if needed
540+
if redis.IsAuthError(cmdErr) {
541+
log.Printf("Auth error in pipeline command %s: %v", cmd.Name(), cmdErr)
542+
} else if redis.IsPermissionError(cmdErr) {
543+
log.Printf("Permission error in pipeline command %s: %v", cmd.Name(), cmdErr)
544+
}
545+
546+
// Optionally wrap the error
547+
wrappedErr := fmt.Errorf("pipeline cmd %s failed: %w", cmd.Name(), cmdErr)
548+
cmd.SetErr(wrappedErr)
549+
}
550+
}
551+
552+
return err
553+
}
554+
}
555+
556+
// Register the hook
557+
rdb.AddHook(PipelineLoggingHook{})
558+
559+
// Use pipeline - errors are still properly typed
560+
pipe := rdb.Pipeline()
561+
pipe.Set(ctx, "key1", "value1", 0)
562+
pipe.Get(ctx, "key2")
563+
_, err := pipe.Exec(ctx)
564+
```
565+
503566
## Run the test
504567
505568
Recommended to use Docker, just need to run:

error.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,33 @@ func IsAskError(err error) (addr string, ok bool) {
315315
return "", false
316316
}
317317

318+
// IsAuthError checks if an error is a Redis authentication error, even if wrapped.
319+
// Authentication errors occur when:
320+
// - NOAUTH: Redis requires authentication but none was provided
321+
// - WRONGPASS: Redis authentication failed due to incorrect password
322+
// - unauthenticated: Error returned when password changed
323+
func IsAuthError(err error) bool {
324+
return proto.IsAuthError(err)
325+
}
326+
327+
// IsPermissionError checks if an error is a Redis permission error, even if wrapped.
328+
// Permission errors (NOPERM) occur when a user does not have permission to execute a command.
329+
func IsPermissionError(err error) bool {
330+
return proto.IsPermissionError(err)
331+
}
332+
333+
// IsExecAbortError checks if an error is a Redis EXECABORT error, even if wrapped.
334+
// EXECABORT errors occur when a transaction is aborted.
335+
func IsExecAbortError(err error) bool {
336+
return proto.IsExecAbortError(err)
337+
}
338+
339+
// IsOOMError checks if an error is a Redis OOM (Out Of Memory) error, even if wrapped.
340+
// OOM errors occur when Redis is out of memory.
341+
func IsOOMError(err error) bool {
342+
return proto.IsOOMError(err)
343+
}
344+
318345
//------------------------------------------------------------------------------
319346

320347
type timeoutError interface {

error_wrapping_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,3 +640,89 @@ func contains(s, substr string) bool {
640640
return strings.Contains(s, substr)
641641
}
642642

643+
func TestAuthErrorWrapping(t *testing.T) {
644+
t.Run("Wrapped NOAUTH error", func(t *testing.T) {
645+
// Create an auth error
646+
authErr := proto.NewAuthError("NOAUTH Authentication required")
647+
648+
// Wrap it
649+
wrappedErr := fmt.Errorf("hook: %w", authErr)
650+
651+
// Should still be detected
652+
if !redis.IsAuthError(wrappedErr) {
653+
t.Errorf("IsAuthError should detect wrapped NOAUTH error")
654+
}
655+
})
656+
657+
t.Run("Wrapped WRONGPASS error", func(t *testing.T) {
658+
// Create an auth error
659+
authErr := proto.NewAuthError("WRONGPASS invalid username-password pair")
660+
661+
// Wrap it multiple times
662+
wrappedErr := fmt.Errorf("connection error: %w", authErr)
663+
doubleWrappedErr := fmt.Errorf("client error: %w", wrappedErr)
664+
665+
// Should still be detected
666+
if !redis.IsAuthError(doubleWrappedErr) {
667+
t.Errorf("IsAuthError should detect double-wrapped WRONGPASS error")
668+
}
669+
})
670+
671+
t.Run("Wrapped unauthenticated error", func(t *testing.T) {
672+
// Create an auth error
673+
authErr := proto.NewAuthError("ERR unauthenticated")
674+
675+
// Wrap it
676+
wrappedErr := fmt.Errorf("hook: %w", authErr)
677+
678+
// Should still be detected
679+
if !redis.IsAuthError(wrappedErr) {
680+
t.Errorf("IsAuthError should detect wrapped unauthenticated error")
681+
}
682+
})
683+
}
684+
685+
func TestPermissionErrorWrapping(t *testing.T) {
686+
t.Run("Wrapped NOPERM error", func(t *testing.T) {
687+
// Create a permission error
688+
permErr := proto.NewPermissionError("NOPERM this user has no permissions to run the 'flushdb' command")
689+
690+
// Wrap it
691+
wrappedErr := fmt.Errorf("hook: %w", permErr)
692+
693+
// Should still be detected
694+
if !redis.IsPermissionError(wrappedErr) {
695+
t.Errorf("IsPermissionError should detect wrapped NOPERM error")
696+
}
697+
})
698+
}
699+
700+
func TestExecAbortErrorWrapping(t *testing.T) {
701+
t.Run("Wrapped EXECABORT error", func(t *testing.T) {
702+
// Create an EXECABORT error
703+
execAbortErr := proto.NewExecAbortError("EXECABORT Transaction discarded because of previous errors")
704+
705+
// Wrap it
706+
wrappedErr := fmt.Errorf("hook: %w", execAbortErr)
707+
708+
// Should still be detected
709+
if !redis.IsExecAbortError(wrappedErr) {
710+
t.Errorf("IsExecAbortError should detect wrapped EXECABORT error")
711+
}
712+
})
713+
}
714+
715+
func TestOOMErrorWrapping(t *testing.T) {
716+
t.Run("Wrapped OOM error", func(t *testing.T) {
717+
// Create an OOM error
718+
oomErr := proto.NewOOMError("OOM command not allowed when used memory > 'maxmemory'")
719+
720+
// Wrap it
721+
wrappedErr := fmt.Errorf("hook: %w", oomErr)
722+
723+
// Should still be detected
724+
if !redis.IsOOMError(wrappedErr) {
725+
t.Errorf("IsOOMError should detect wrapped OOM error")
726+
}
727+
})
728+
}

0 commit comments

Comments
 (0)