Search code examples
gokubernetesetcdclient-gokubernetes-custom-resources

How to return a custom user friendly error message in Kubernetes?


I have a backend with golang that talks to k8s. I want to reformulate the error response that i get from k8s and send it to the frontend.

I want to return a meaningful validation error messages for the user, when he add a non valid name, something already exist ...

And i want something generic not hardcoded in each endpoint's controller.

I am using kubernetes/client-go.

  1. First error:

For example lets say i want to add a hotel to the etcd, when i try to add the hotel's name: hotel123, that's already exist.

  • I get this error message: \"hotel123\" already exists.
  • What i want : hotel123 already exists.
  1. second error:

For example lets say i want to add a hotel to the etcd, when i try to add the hotel name: hotel_123, that's alerady exist.

  • I get this error message: \"hotel_123\" is invalid, Invalid value: \"hotel_123\"...
  • What i want: hotel_123 is invalid

How to return a custom user friendly error message ?

PS: i have multiple functions, so the validation should be generic.


Solution

  • In general (although there are workarounds), if you want to trap an error in order to return a more useful error, you want to ensure the following conditions are met:

    1. The error you're trapping has a meaningful type
    2. You're using go version >= 1.13 which ships with useful helper functions

    In the following example I'm trying to read a config file that doesn't exist. My code checks that the error returned is a fs.PathError and then throws it's own more useful error. You can extend this general idea to your use case.

    package main
    
    import (
        "errors"
        "fmt"
        "io/fs"
    
        "k8s.io/client-go/tools/clientcmd"
    )
    
    func main() {
    
        var myError error
        config, originalError := clientcmd.BuildConfigFromFlags("", "/some/path/that/doesnt/exist")
        if originalError != nil {
    
            var pathError *fs.PathError
    
            switch {
            case errors.As(originalError, &pathError):
    
                myError = fmt.Errorf("there is no config file at %s", originalError.(*fs.PathError).Path)
    
            default:
    
                myError = fmt.Errorf("there was an error and it's type was %T", originalError)
    
            }
    
            fmt.Printf("%#v", myError)
    
        } else {
    
            fmt.Println("There was no error")
            fmt.Println(config)
    
        }
    
    }
    

    In your debugging, you will find the %T formatter useful.

    For your specific use-case, you can use a Regex to parse out the desired text.

    The regex below says:

    1. ^\W* start with any non-alhpanumeric characters
    2. (\w+) capture the alphanumeric string following
    3. \W*\s? match non-alphanumeric characters
    4. (is\sinvalid) capture "is invalid"
    func MyError(inError error) error {
        pattern, _ := regexp.Compile(`^\W*(\w+)\W*\s?(is\sinvalid)(.*)$`)
        myErrorString := pattern.ReplaceAll([]byte(inError.Error()), []byte("$1 $2"))
        return errors.New(string(myErrorString))
    }
    

    As seen on this playground:

    https://goplay.tools/snippet/bcZO7wa8Vnl