mautrix-go/error.go

158 lines
5.5 KiB
Go

// Copyright (c) 2020 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package mautrix
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"golang.org/x/exp/maps"
)
// Common error codes from https://matrix.org/docs/spec/client_server/latest#api-standards
//
// Can be used with errors.Is() to check the response code without casting the error:
//
// err := client.Sync()
// if errors.Is(err, MUnknownToken) {
// // logout
// }
var (
// Forbidden access, e.g. joining a room without permission, failed login.
MForbidden = RespError{ErrCode: "M_FORBIDDEN"}
// Unrecognized request, e.g. the endpoint does not exist or is not implemented.
MUnrecognized = RespError{ErrCode: "M_UNRECOGNIZED"}
// The access token specified was not recognised.
MUnknownToken = RespError{ErrCode: "M_UNKNOWN_TOKEN"}
// No access token was specified for the request.
MMissingToken = RespError{ErrCode: "M_MISSING_TOKEN"}
// Request contained valid JSON, but it was malformed in some way, e.g. missing required keys, invalid values for keys.
MBadJSON = RespError{ErrCode: "M_BAD_JSON"}
// Request did not contain valid JSON.
MNotJSON = RespError{ErrCode: "M_NOT_JSON"}
// No resource was found for this request.
MNotFound = RespError{ErrCode: "M_NOT_FOUND"}
// Too many requests have been sent in a short period of time. Wait a while then try again.
MLimitExceeded = RespError{ErrCode: "M_LIMIT_EXCEEDED"}
// The user ID associated with the request has been deactivated.
// Typically for endpoints that prove authentication, such as /login.
MUserDeactivated = RespError{ErrCode: "M_USER_DEACTIVATED"}
// Encountered when trying to register a user ID which has been taken.
MUserInUse = RespError{ErrCode: "M_USER_IN_USE"}
// Encountered when trying to register a user ID which is not valid.
MInvalidUsername = RespError{ErrCode: "M_INVALID_USERNAME"}
// Sent when the room alias given to the createRoom API is already in use.
MRoomInUse = RespError{ErrCode: "M_ROOM_IN_USE"}
// The state change requested cannot be performed, such as attempting to unban a user who is not banned.
MBadState = RespError{ErrCode: "M_BAD_STATE"}
// The request or entity was too large.
MTooLarge = RespError{ErrCode: "M_TOO_LARGE"}
// The resource being requested is reserved by an application service, or the application service making the request has not created the resource.
MExclusive = RespError{ErrCode: "M_EXCLUSIVE"}
// The client's request to create a room used a room version that the server does not support.
MUnsupportedRoomVersion = RespError{ErrCode: "M_UNSUPPORTED_ROOM_VERSION"}
// The client attempted to join a room that has a version the server does not support.
// Inspect the room_version property of the error response for the room's version.
MIncompatibleRoomVersion = RespError{ErrCode: "M_INCOMPATIBLE_ROOM_VERSION"}
// The client specified a parameter that has the wrong value.
MInvalidParam = RespError{ErrCode: "M_INVALID_PARAM"}
MURLNotSet = RespError{ErrCode: "M_URL_NOT_SET"}
MBadStatus = RespError{ErrCode: "M_BAD_STATUS"}
MConnectionTimeout = RespError{ErrCode: "M_CONNECTION_TIMEOUT"}
MConnectionFailed = RespError{ErrCode: "M_CONNECTION_FAILED"}
)
// HTTPError An HTTP Error response, which may wrap an underlying native Go Error.
type HTTPError struct {
Request *http.Request
Response *http.Response
ResponseBody string
WrappedError error
RespError *RespError
Message string
}
func (e HTTPError) Is(err error) bool {
return (e.RespError != nil && errors.Is(e.RespError, err)) || (e.WrappedError != nil && errors.Is(e.WrappedError, err))
}
func (e HTTPError) IsStatus(code int) bool {
return e.Response != nil && e.Response.StatusCode == code
}
func (e HTTPError) Error() string {
if e.WrappedError != nil {
return fmt.Sprintf("%s: %v", e.Message, e.WrappedError)
} else if e.RespError != nil {
return fmt.Sprintf("failed to %s %s: %s (HTTP %d): %s", e.Request.Method, e.Request.URL.Path,
e.RespError.ErrCode, e.Response.StatusCode, e.RespError.Err)
} else {
msg := fmt.Sprintf("failed to %s %s: HTTP %d", e.Request.Method, e.Request.URL.Path, e.Response.StatusCode)
if len(e.ResponseBody) > 0 {
msg = fmt.Sprintf("%s: %s", msg, e.ResponseBody)
}
return msg
}
}
func (e HTTPError) Unwrap() error {
if e.WrappedError != nil {
return e.WrappedError
} else if e.RespError != nil {
return *e.RespError
}
return nil
}
// RespError is the standard JSON error response from Homeservers. It also implements the Golang "error" interface.
// See https://spec.matrix.org/v1.2/client-server-api/#api-standards
type RespError struct {
ErrCode string
Err string
ExtraData map[string]interface{}
}
func (e *RespError) UnmarshalJSON(data []byte) error {
err := json.Unmarshal(data, &e.ExtraData)
if err != nil {
return err
}
e.ErrCode, _ = e.ExtraData["errcode"].(string)
e.Err, _ = e.ExtraData["error"].(string)
return nil
}
func (e *RespError) MarshalJSON() ([]byte, error) {
data := maps.Clone(e.ExtraData)
if data == nil {
data = make(map[string]any)
}
data["errcode"] = e.ErrCode
data["error"] = e.Err
return json.Marshal(data)
}
// Error returns the errcode and error message.
func (e RespError) Error() string {
return e.ErrCode + ": " + e.Err
}
func (e RespError) Is(err error) bool {
e2, ok := err.(RespError)
if !ok {
return false
}
if e.ErrCode == "M_UNKNOWN" && e2.ErrCode == "M_UNKNOWN" {
return e.Err == e2.Err
}
return e2.ErrCode == e.ErrCode
}