mirror of https://github.com/mautrix/go.git
187 lines
6.7 KiB
Go
187 lines
6.7 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"
|
|
|
|
"go.mau.fi/util/exhttp"
|
|
"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 (
|
|
// Generic error for when the server encounters an error and it does not have a more specific error code.
|
|
// Note that `errors.Is` will check the error message rather than code for M_UNKNOWNs.
|
|
MUnknown = RespError{ErrCode: "M_UNKNOWN", StatusCode: http.StatusInternalServerError}
|
|
// Forbidden access, e.g. joining a room without permission, failed login.
|
|
MForbidden = RespError{ErrCode: "M_FORBIDDEN", StatusCode: http.StatusForbidden}
|
|
// Unrecognized request, e.g. the endpoint does not exist or is not implemented.
|
|
MUnrecognized = RespError{ErrCode: "M_UNRECOGNIZED", StatusCode: http.StatusNotFound}
|
|
// The access token specified was not recognised.
|
|
MUnknownToken = RespError{ErrCode: "M_UNKNOWN_TOKEN", StatusCode: http.StatusUnauthorized}
|
|
// No access token was specified for the request.
|
|
MMissingToken = RespError{ErrCode: "M_MISSING_TOKEN", StatusCode: http.StatusUnauthorized}
|
|
// 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", StatusCode: http.StatusBadRequest}
|
|
// Request did not contain valid JSON.
|
|
MNotJSON = RespError{ErrCode: "M_NOT_JSON", StatusCode: http.StatusBadRequest}
|
|
// No resource was found for this request.
|
|
MNotFound = RespError{ErrCode: "M_NOT_FOUND", StatusCode: http.StatusNotFound}
|
|
// Too many requests have been sent in a short period of time. Wait a while then try again.
|
|
MLimitExceeded = RespError{ErrCode: "M_LIMIT_EXCEEDED", StatusCode: http.StatusTooManyRequests}
|
|
// 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", StatusCode: http.StatusBadRequest}
|
|
// Encountered when trying to register a user ID which is not valid.
|
|
MInvalidUsername = RespError{ErrCode: "M_INVALID_USERNAME", StatusCode: http.StatusBadRequest}
|
|
// Sent when the room alias given to the createRoom API is already in use.
|
|
MRoomInUse = RespError{ErrCode: "M_ROOM_IN_USE", StatusCode: http.StatusBadRequest}
|
|
// 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", StatusCode: http.StatusRequestEntityTooLarge}
|
|
// 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", StatusCode: http.StatusBadRequest}
|
|
// 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", StatusCode: http.StatusBadRequest}
|
|
|
|
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("%s (HTTP %d): %s", e.RespError.ErrCode, e.Response.StatusCode, e.RespError.Err)
|
|
} else {
|
|
msg := fmt.Sprintf("HTTP %d", 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]any
|
|
|
|
StatusCode int
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func (e RespError) Write(w http.ResponseWriter) {
|
|
if w == nil {
|
|
return
|
|
}
|
|
statusCode := e.StatusCode
|
|
if statusCode == 0 {
|
|
statusCode = http.StatusInternalServerError
|
|
}
|
|
exhttp.WriteJSONResponse(w, statusCode, &e)
|
|
}
|
|
|
|
func (e RespError) WithMessage(msg string, args ...any) RespError {
|
|
if len(args) > 0 {
|
|
msg = fmt.Sprintf(msg, args...)
|
|
}
|
|
e.Err = msg
|
|
return e
|
|
}
|
|
|
|
func (e RespError) WithStatus(status int) RespError {
|
|
e.StatusCode = status
|
|
return e
|
|
}
|
|
|
|
// 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
|
|
}
|