1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
|
// 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("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]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) {
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
}
|