Scaffolds the server module and everything Phase 0 of PLAN.md calls for: a binary that boots, serves its discovery endpoints and nothing else yet. - config: one YAML file plus per-field WARP_* environment overrides, unknown fields rejected, validation that reports every problem at once - logging and metrics: slog, a Prometheus registry, per-route HTTP instrumentation labelled by pattern rather than path - httputil: the Matrix error envelope, error-returning handlers, CORS, request logging, panic recovery, and a router over http.ServeMux - endpoints: /_matrix/client/versions, /.well-known/matrix/client, /.well-known/matrix/server (gated), /healthz, /metrics, and M_UNRECOGNIZED for everything else - storage: driver registry, shared database/sql base, checksummed migration runner, sqlite and postgres drivers, pocketbase stub - settings: server_settings table behind a cached service, so the federation and registration switches change without a restart - federation: the gate that makes federation a runtime mode - cmd/warp: serve, migrate, generate-keys, version - Makefile, golangci-lint v2, GitHub Actions CI (Linux and Windows tests, race on Linux, storage tests against PostgreSQL, lint, boot smoke test), Dockerfile, example config and docs Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
133 lines
4.5 KiB
Go
133 lines
4.5 KiB
Go
// 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"
|
|
)
|
|
|
|
// 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")
|
|
}
|