Reorganize errors
This commit is contained in:
106
common/errors.go
106
common/errors.go
@@ -1,106 +0,0 @@
|
|||||||
package common
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GeminiError struct {
|
|
||||||
Msg string
|
|
||||||
Code int
|
|
||||||
Header string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *GeminiError) Error() string {
|
|
||||||
return fmt.Sprintf("%s: %s", e.Msg, e.Header)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewErrGeminiStatusCode(code int, header string) error {
|
|
||||||
var msg string
|
|
||||||
switch {
|
|
||||||
case code >= 10 && code < 20:
|
|
||||||
msg = "needs input"
|
|
||||||
case code >= 30 && code < 40:
|
|
||||||
msg = "redirect"
|
|
||||||
case code >= 40 && code < 50:
|
|
||||||
msg = "bad request"
|
|
||||||
case code >= 50 && code < 60:
|
|
||||||
msg = "server error"
|
|
||||||
case code >= 60 && code < 70:
|
|
||||||
msg = "TLS error"
|
|
||||||
default:
|
|
||||||
msg = "unexpected Status code"
|
|
||||||
}
|
|
||||||
return &GeminiError{
|
|
||||||
Msg: msg,
|
|
||||||
Code: code,
|
|
||||||
Header: header,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrGeminiRobotsParse = errors.New("gemini robots.txt parse error")
|
|
||||||
ErrGeminiRobotsDisallowed = errors.New("gemini robots.txt disallowed")
|
|
||||||
ErrGeminiResponseHeader = errors.New("gemini response header error")
|
|
||||||
ErrGeminiRedirect = errors.New("gemini redirection error")
|
|
||||||
ErrGeminiLinkLineParse = errors.New("gemini link line parse error")
|
|
||||||
|
|
||||||
ErrURLParse = errors.New("URL parse error")
|
|
||||||
ErrURLNotGemini = errors.New("not a Gemini URL")
|
|
||||||
ErrURLDecode = errors.New("URL decode error")
|
|
||||||
ErrUTF8Parse = errors.New("UTF-8 parse error")
|
|
||||||
ErrTextParse = errors.New("text parse error")
|
|
||||||
|
|
||||||
ErrBlacklistMatches = errors.New("url matches blacklist")
|
|
||||||
|
|
||||||
ErrNetwork = errors.New("network error")
|
|
||||||
ErrNetworkDNS = errors.New("network DNS error")
|
|
||||||
ErrNetworkTLS = errors.New("network TLS error")
|
|
||||||
ErrNetworkSetConnectionDeadline = errors.New("network error - cannot set connection deadline")
|
|
||||||
ErrNetworkCannotWrite = errors.New("network error - cannot write")
|
|
||||||
ErrNetworkResponseSizeExceededMax = errors.New("network error - response size exceeded maximum size")
|
|
||||||
|
|
||||||
ErrDatabase = errors.New("database error")
|
|
||||||
ErrDatabaseScan = errors.New("database scan error")
|
|
||||||
)
|
|
||||||
|
|
||||||
// We could have used a map for speed, but
|
|
||||||
// we would lose ability to check wrapped
|
|
||||||
// errors via errors.Is().
|
|
||||||
|
|
||||||
var errGemini *GeminiError
|
|
||||||
|
|
||||||
var knownErrors = []error{ //nolint:gochecknoglobals
|
|
||||||
errGemini,
|
|
||||||
ErrGeminiLinkLineParse,
|
|
||||||
ErrGeminiRobotsParse,
|
|
||||||
ErrGeminiRobotsDisallowed,
|
|
||||||
ErrGeminiResponseHeader,
|
|
||||||
ErrGeminiRedirect,
|
|
||||||
|
|
||||||
ErrBlacklistMatches,
|
|
||||||
|
|
||||||
ErrURLParse,
|
|
||||||
ErrURLDecode,
|
|
||||||
ErrUTF8Parse,
|
|
||||||
ErrTextParse,
|
|
||||||
|
|
||||||
ErrNetwork,
|
|
||||||
ErrNetworkDNS,
|
|
||||||
ErrNetworkTLS,
|
|
||||||
ErrNetworkSetConnectionDeadline,
|
|
||||||
ErrNetworkCannotWrite,
|
|
||||||
ErrNetworkResponseSizeExceededMax,
|
|
||||||
|
|
||||||
ErrDatabase,
|
|
||||||
ErrDatabaseScan,
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsKnownError(err error) bool {
|
|
||||||
for _, known := range knownErrors {
|
|
||||||
if errors.Is(err, known) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return errors.As(err, new(*GeminiError))
|
|
||||||
}
|
|
||||||
41
common/errors/errors.go
Normal file
41
common/errors/errors.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gemini-grc/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HostError is an error encountered while
|
||||||
|
// visiting a host, and should be recorded
|
||||||
|
// to the snapshot.
|
||||||
|
type HostError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HostError) Error() string {
|
||||||
|
return e.Err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HostError) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHostError(err error) error {
|
||||||
|
return &HostError{Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsHostError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var asError *HostError
|
||||||
|
return errors.As(err, &asError)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sentinel errors used for their string message primarily.
|
||||||
|
// Do not use them by themselves, to be embedded to HostError.
|
||||||
|
var (
|
||||||
|
ErrBlacklistMatch = fmt.Errorf("black list match")
|
||||||
|
ErrRobotsMatch = fmt.Errorf("robots match")
|
||||||
|
)
|
||||||
38
common/errors/errors_test.go
Normal file
38
common/errors/errors_test.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
package errors_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gemini-grc/gemini"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestErrGemini(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
err := gemini.NewGeminiError(50, "50 server error")
|
||||||
|
if !errors.As(err, new(*gemini.GeminiError)) {
|
||||||
|
t.Errorf("TestErrGemini fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrGeminiWrapped(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
err := gemini.NewGeminiError(50, "50 server error")
|
||||||
|
errWrapped := fmt.Errorf("%w wrapped", err)
|
||||||
|
if !errors.As(errWrapped, new(*gemini.GeminiError)) {
|
||||||
|
t.Errorf("TestErrGeminiWrapped fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsGeminiError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
err1 := gemini.NewGeminiError(50, "50 server error")
|
||||||
|
if !gemini.IsGeminiError(err1) {
|
||||||
|
t.Errorf("TestGeminiError fail #1")
|
||||||
|
}
|
||||||
|
wrappedErr1 := fmt.Errorf("wrapped %w", err1)
|
||||||
|
if !gemini.IsGeminiError(wrappedErr1) {
|
||||||
|
t.Errorf("TestGeminiError fail #2")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package common_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"gemini-grc/common"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestErrGemini(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
err := common.NewErrGeminiStatusCode(50, "50 server error")
|
|
||||||
if !errors.As(err, new(*common.GeminiError)) {
|
|
||||||
t.Errorf("TestErrGemini fail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestErrGeminiWrapped(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
err := common.NewErrGeminiStatusCode(50, "50 server error")
|
|
||||||
errWrapped := fmt.Errorf("%w wrapped", err)
|
|
||||||
if !errors.As(errWrapped, new(*common.GeminiError)) {
|
|
||||||
t.Errorf("TestErrGeminiWrapped fail")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
104
errors/errors.go
Normal file
104
errors/errors.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fatal interface {
|
||||||
|
Fatal() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsFatal(err error) bool {
|
||||||
|
var e fatal
|
||||||
|
ok := As(err, &e)
|
||||||
|
return ok && e.Fatal()
|
||||||
|
}
|
||||||
|
|
||||||
|
func As(err error, target any) bool {
|
||||||
|
return errors.As(err, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Is(err, target error) bool {
|
||||||
|
return errors.Is(err, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Unwrap(err error) error {
|
||||||
|
return errors.Unwrap(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Err error
|
||||||
|
Stack string
|
||||||
|
fatal bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("%v\n", e.Err))
|
||||||
|
sb.WriteString(fmt.Sprintf("Stack Trace:\n%s", e.Stack))
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Fatal() bool {
|
||||||
|
return e.fatal
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Unwrap() error {
|
||||||
|
return e.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewError(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's already of our own
|
||||||
|
// Error type, so we don't add stack twice.
|
||||||
|
var asError *Error
|
||||||
|
if errors.As(err, &asError) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// has the stack trace
|
||||||
|
var stack strings.Builder
|
||||||
|
buf := make([]uintptr, 50)
|
||||||
|
n := runtime.Callers(2, buf)
|
||||||
|
frames := runtime.CallersFrames(buf[:n])
|
||||||
|
|
||||||
|
// Format the stack trace
|
||||||
|
for {
|
||||||
|
frame, more := frames.Next()
|
||||||
|
// Skip runtime and standard library frames
|
||||||
|
if !strings.Contains(frame.File, "runtime/") {
|
||||||
|
stack.WriteString(fmt.Sprintf("\t%s:%d - %s\n", frame.File, frame.Line, frame.Function))
|
||||||
|
}
|
||||||
|
if !more {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Error{
|
||||||
|
Err: err,
|
||||||
|
Stack: stack.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFatalError(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's already of our own
|
||||||
|
// Error type.
|
||||||
|
var asError *Error
|
||||||
|
if errors.As(err, &asError) {
|
||||||
|
asError.fatal = true // Set fatal even for existing Error types
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err2 := NewError(err)
|
||||||
|
err2.(*Error).fatal = true
|
||||||
|
return err2
|
||||||
|
}
|
||||||
184
errors/errors_test.go
Normal file
184
errors/errors_test.go
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CustomError struct {
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *CustomError) Error() string { return e.Err.Error() }
|
||||||
|
|
||||||
|
func IsCustomError(err error) bool {
|
||||||
|
var asError *CustomError
|
||||||
|
return errors.As(err, &asError)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapping(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
originalErr := errors.New("original error")
|
||||||
|
err1 := NewError(originalErr)
|
||||||
|
if !errors.Is(err1, originalErr) {
|
||||||
|
t.Errorf("original error is not wrapped")
|
||||||
|
}
|
||||||
|
if !Is(err1, originalErr) {
|
||||||
|
t.Errorf("original error is not wrapped")
|
||||||
|
}
|
||||||
|
unwrappedErr := errors.Unwrap(err1)
|
||||||
|
if !errors.Is(unwrappedErr, originalErr) {
|
||||||
|
t.Errorf("original error is not wrapped")
|
||||||
|
}
|
||||||
|
if !Is(unwrappedErr, originalErr) {
|
||||||
|
t.Errorf("original error is not wrapped")
|
||||||
|
}
|
||||||
|
unwrappedErr = Unwrap(err1)
|
||||||
|
if !errors.Is(unwrappedErr, originalErr) {
|
||||||
|
t.Errorf("original error is not wrapped")
|
||||||
|
}
|
||||||
|
if !Is(unwrappedErr, originalErr) {
|
||||||
|
t.Errorf("original error is not wrapped")
|
||||||
|
}
|
||||||
|
wrappedErr := fmt.Errorf("wrapped: %w", originalErr)
|
||||||
|
if !errors.Is(wrappedErr, originalErr) {
|
||||||
|
t.Errorf("original error is not wrapped")
|
||||||
|
}
|
||||||
|
if !Is(wrappedErr, originalErr) {
|
||||||
|
t.Errorf("original error is not wrapped")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewError(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
originalErr := &CustomError{errors.New("err1")}
|
||||||
|
if !IsCustomError(originalErr) {
|
||||||
|
t.Errorf("TestNewError fail #1")
|
||||||
|
}
|
||||||
|
err1 := NewError(originalErr)
|
||||||
|
if !IsCustomError(err1) {
|
||||||
|
t.Errorf("TestNewError fail #2")
|
||||||
|
}
|
||||||
|
wrappedErr1 := fmt.Errorf("wrapped %w", err1)
|
||||||
|
if !IsCustomError(wrappedErr1) {
|
||||||
|
t.Errorf("TestNewError fail #3")
|
||||||
|
}
|
||||||
|
unwrappedErr1 := Unwrap(wrappedErr1)
|
||||||
|
if !IsCustomError(unwrappedErr1) {
|
||||||
|
t.Errorf("TestNewError fail #4")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsFatal(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil error",
|
||||||
|
err: nil,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple non-fatal error",
|
||||||
|
err: fmt.Errorf("regular error"),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "direct fatal error",
|
||||||
|
err: NewFatalError(fmt.Errorf("fatal error")),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-fatal Error type",
|
||||||
|
err: NewError(fmt.Errorf("non-fatal error")),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrapped fatal error - one level",
|
||||||
|
err: fmt.Errorf("outer: %w", NewFatalError(fmt.Errorf("inner fatal"))),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrapped fatal error - two levels",
|
||||||
|
err: fmt.Errorf("outer: %w",
|
||||||
|
fmt.Errorf("middle: %w",
|
||||||
|
NewFatalError(fmt.Errorf("inner fatal")))),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrapped fatal error - three levels",
|
||||||
|
err: fmt.Errorf("outer: %w",
|
||||||
|
fmt.Errorf("middle1: %w",
|
||||||
|
fmt.Errorf("middle2: %w",
|
||||||
|
NewFatalError(fmt.Errorf("inner fatal"))))),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple wrapped errors - non-fatal",
|
||||||
|
err: fmt.Errorf("outer: %w",
|
||||||
|
fmt.Errorf("middle: %w",
|
||||||
|
fmt.Errorf("inner: %w",
|
||||||
|
NewError(fmt.Errorf("base error"))))),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrapped non-fatal Error type",
|
||||||
|
err: fmt.Errorf("outer: %w", NewError(fmt.Errorf("inner"))),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrapped basic error",
|
||||||
|
err: fmt.Errorf("outer: %w", fmt.Errorf("inner")),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fatal error wrapping fatal error",
|
||||||
|
err: NewFatalError(NewFatalError(fmt.Errorf("double fatal"))),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fatal error wrapping non-fatal Error",
|
||||||
|
err: NewFatalError(NewError(fmt.Errorf("mixed"))),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-fatal Error wrapping fatal error",
|
||||||
|
err: NewError(NewFatalError(fmt.Errorf("mixed"))),
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Error wrapping Error",
|
||||||
|
err: NewError(NewError(fmt.Errorf("double wrapped"))),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrapped nil error",
|
||||||
|
err: fmt.Errorf("outer: %w", nil),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "fatal wrapping nil",
|
||||||
|
err: NewFatalError(nil),
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
tt := tt
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
got := IsFatal(tt.err)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("IsFatal() = %v, want %v", got, tt.want)
|
||||||
|
if tt.err != nil {
|
||||||
|
t.Errorf("Error was: %v", tt.err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
52
gemini/errors.go
Normal file
52
gemini/errors.go
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package gemini
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"gemini-grc/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GeminiError is used to represent
|
||||||
|
// Gemini network protocol errors only.
|
||||||
|
// Should be recorded to the snapshot.
|
||||||
|
// See https://geminiprotocol.net/docs/protocol-specification.gmi
|
||||||
|
type GeminiError struct {
|
||||||
|
Msg string
|
||||||
|
Code int
|
||||||
|
Header string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *GeminiError) Error() string {
|
||||||
|
return fmt.Sprintf("gemini error: code %d %s", e.Code, e.Msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGeminiError(code int, header string) error {
|
||||||
|
var msg string
|
||||||
|
switch {
|
||||||
|
case code >= 10 && code < 20:
|
||||||
|
msg = "needs input"
|
||||||
|
case code >= 30 && code < 40:
|
||||||
|
msg = "redirect"
|
||||||
|
case code >= 40 && code < 50:
|
||||||
|
msg = "bad request"
|
||||||
|
case code >= 50 && code < 60:
|
||||||
|
msg = "server error"
|
||||||
|
case code >= 60 && code < 70:
|
||||||
|
msg = "TLS error"
|
||||||
|
default:
|
||||||
|
msg = "unexpected Status code"
|
||||||
|
}
|
||||||
|
return &GeminiError{
|
||||||
|
Msg: msg,
|
||||||
|
Code: code,
|
||||||
|
Header: header,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsGeminiError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var asError *GeminiError
|
||||||
|
return errors.As(err, &asError)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user