// Package httputil holds the HTTP plumbing shared by every Warp API: the // Matrix error envelope, JSON helpers, CORS and middleware. package httputil import ( "fmt" "net/http" ) // Matrix error codes used so far. The full list lives in the Client-Server // API spec, section "Standard error response"; codes are added as the // endpoints that return them are implemented. const ( ErrCodeForbidden = "M_FORBIDDEN" ErrCodeUnknownToken = "M_UNKNOWN_TOKEN" ErrCodeMissingToken = "M_MISSING_TOKEN" ErrCodeBadJSON = "M_BAD_JSON" ErrCodeNotJSON = "M_NOT_JSON" ErrCodeNotFound = "M_NOT_FOUND" ErrCodeLimitExceeded = "M_LIMIT_EXCEEDED" ErrCodeUnknown = "M_UNKNOWN" ErrCodeUnrecognized = "M_UNRECOGNIZED" ErrCodeUnauthorized = "M_UNAUTHORIZED" ErrCodeMissingParam = "M_MISSING_PARAM" ErrCodeInvalidParam = "M_INVALID_PARAM" ErrCodeTooLarge = "M_TOO_LARGE" ErrCodeUnsupportedVer = "M_UNSUPPORTED_ROOM_VERSION" // Room codes, added with the roomserver in Phase 3. ErrCodeRoomInUse = "M_ROOM_IN_USE" // Account and session codes, added with the userapi in Phase 1. ErrCodeUserInUse = "M_USER_IN_USE" ErrCodeInvalidUsername = "M_INVALID_USERNAME" ErrCodeUserDeactivated = "M_USER_DEACTIVATED" ErrCodeExclusive = "M_EXCLUSIVE" ErrCodeWeakPassword = "M_WEAK_PASSWORD" ) // MatrixError is the standard Matrix error envelope. Every non-2xx response // from a /_matrix endpoint is one of these, serialised as // // {"errcode": "M_FORBIDDEN", "error": "..."} // // It implements error so handlers can return it up the stack. type MatrixError struct { Code string `json:"errcode"` Message string `json:"error"` // RetryAfterMS accompanies M_LIMIT_EXCEEDED. RetryAfterMS int64 `json:"retry_after_ms,omitempty"` // SoftLogout accompanies M_UNKNOWN_TOKEN when the client may keep its // encryption state and re-login. SoftLogout bool `json:"soft_logout,omitempty"` // status is the HTTP status to send; not part of the JSON body. status int } func (e *MatrixError) Error() string { return fmt.Sprintf("%s: %s", e.Code, e.Message) } // StatusCode is the HTTP status this error is sent with. func (e *MatrixError) StatusCode() int { if e.status == 0 { return http.StatusInternalServerError } return e.status } // NewError builds an arbitrary Matrix error. func NewError(status int, code, format string, args ...any) *MatrixError { return &MatrixError{Code: code, Message: fmt.Sprintf(format, args...), status: status} } // Forbidden is M_FORBIDDEN (403). Federation-disabled rejections use it. func Forbidden(format string, args ...any) *MatrixError { return NewError(http.StatusForbidden, ErrCodeForbidden, format, args...) } // NotFound is M_NOT_FOUND (404) for a resource that could exist but does not. func NotFound(format string, args ...any) *MatrixError { return NewError(http.StatusNotFound, ErrCodeNotFound, format, args...) } // Unrecognized is M_UNRECOGNIZED (404), the spec's answer for an endpoint // this server does not implement at all. func Unrecognized() *MatrixError { return NewError(http.StatusNotFound, ErrCodeUnrecognized, "Unrecognized request") } // MethodNotAllowed is M_UNRECOGNIZED (405) for a known path, wrong verb. func MethodNotAllowed() *MatrixError { return NewError(http.StatusMethodNotAllowed, ErrCodeUnrecognized, "Unrecognized request") } // NotJSON is M_NOT_JSON (400): the body was not valid JSON at all. func NotJSON() *MatrixError { return NewError(http.StatusBadRequest, ErrCodeNotJSON, "Content not JSON") } // BadJSON is M_BAD_JSON (400): valid JSON, wrong shape. func BadJSON(format string, args ...any) *MatrixError { return NewError(http.StatusBadRequest, ErrCodeBadJSON, format, args...) } // MissingParam is M_MISSING_PARAM (400). func MissingParam(format string, args ...any) *MatrixError { return NewError(http.StatusBadRequest, ErrCodeMissingParam, format, args...) } // InvalidParam is M_INVALID_PARAM (400). func InvalidParam(format string, args ...any) *MatrixError { return NewError(http.StatusBadRequest, ErrCodeInvalidParam, format, args...) } // MissingToken is M_MISSING_TOKEN (401). func MissingToken() *MatrixError { return NewError(http.StatusUnauthorized, ErrCodeMissingToken, "Missing access token") } // UnknownToken is M_UNKNOWN_TOKEN (401). func UnknownToken(softLogout bool) *MatrixError { err := NewError(http.StatusUnauthorized, ErrCodeUnknownToken, "Unrecognised access token") err.SoftLogout = softLogout return err } // LimitExceeded is M_LIMIT_EXCEEDED (429). func LimitExceeded(retryAfterMS int64) *MatrixError { err := NewError(http.StatusTooManyRequests, ErrCodeLimitExceeded, "Too many requests") err.RetryAfterMS = retryAfterMS return err } // InternalError is M_UNKNOWN (500). The message is deliberately vague: the // detail belongs in the log, not in the response. func InternalError() *MatrixError { return NewError(http.StatusInternalServerError, ErrCodeUnknown, "Internal server error") } // UserInUse is M_USER_IN_USE (400): the requested user name is taken. func UserInUse() *MatrixError { return NewError(http.StatusBadRequest, ErrCodeUserInUse, "Desired user ID is already taken") } // InvalidUsername is M_INVALID_USERNAME (400): the user name is not usable, // either because of its characters or its length. func InvalidUsername(format string, args ...any) *MatrixError { return NewError(http.StatusBadRequest, ErrCodeInvalidUsername, format, args...) } // UserDeactivated is M_USER_DEACTIVATED (403). func UserDeactivated() *MatrixError { return NewError(http.StatusForbidden, ErrCodeUserDeactivated, "This account has been deactivated") } // Unknown is M_UNKNOWN with an explicit status, for the handful of cases the // spec answers with that code rather than a specific one — an unsupported // login type, for instance. func Unknown(status int, format string, args ...any) *MatrixError { return NewError(status, ErrCodeUnknown, format, args...) }