summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-11-13 19:48:51 +0100
committerLibravatar GitHub <noreply@github.com>2023-11-13 19:48:51 +0100
commit8d0c017cf205bcf57630c91b53079001deed4d36 (patch)
treecf2240e3dff137df2871c9b93548a885833e9a52
parent[chore] update otel -> v1.20.0 (#2358) (diff)
downloadgotosocial-8d0c017cf205bcf57630c91b53079001deed4d36.tar.xz
[feature/performance] Wrap incoming HTTP requests in timeout handler (#2353)
* deinterface router, start messing about with deadlines * weeeee * thanks linter (thinter) * write Connection: close when timing out requests * update wording * don't replace req * don't bother with fancy Cause functions (I'll use them one day...)
-rw-r--r--internal/api/activitypub.go4
-rw-r--r--internal/api/auth.go2
-rw-r--r--internal/api/client.go2
-rw-r--r--internal/api/fileserver.go2
-rw-r--r--internal/api/nodeinfo.go2
-rw-r--r--internal/api/util/errorhandling.go46
-rw-r--r--internal/api/wellknown.go2
-rw-r--r--internal/gotosocial/gotosocial.go4
-rw-r--r--internal/gtserror/withcode.go11
-rw-r--r--internal/router/attach.go8
-rw-r--r--internal/router/router.go326
-rw-r--r--internal/router/timeout.go61
-rw-r--r--internal/web/web.go2
-rw-r--r--testrig/router.go2
14 files changed, 302 insertions, 172 deletions
diff --git a/internal/api/activitypub.go b/internal/api/activitypub.go
index 02ae0767c..0c0222d1c 100644
--- a/internal/api/activitypub.go
+++ b/internal/api/activitypub.go
@@ -35,7 +35,7 @@ type ActivityPub struct {
signatureCheckMiddleware gin.HandlerFunc
}
-func (a *ActivityPub) Route(r router.Router, m ...gin.HandlerFunc) {
+func (a *ActivityPub) Route(r *router.Router, m ...gin.HandlerFunc) {
// create groupings for the 'emoji' and 'users' prefixes
emojiGroup := r.AttachGroup("emoji")
usersGroup := r.AttachGroup("users")
@@ -54,7 +54,7 @@ func (a *ActivityPub) Route(r router.Router, m ...gin.HandlerFunc) {
}
// Public key endpoint requires different middleware + cache policies from other AP endpoints.
-func (a *ActivityPub) RoutePublicKey(r router.Router, m ...gin.HandlerFunc) {
+func (a *ActivityPub) RoutePublicKey(r *router.Router, m ...gin.HandlerFunc) {
// Create grouping for the 'users/[username]/main-key' prefix.
publicKeyGroup := r.AttachGroup(publickey.PublicKeyPath)
diff --git a/internal/api/auth.go b/internal/api/auth.go
index 961caa981..8d7808a3a 100644
--- a/internal/api/auth.go
+++ b/internal/api/auth.go
@@ -36,7 +36,7 @@ type Auth struct {
}
// Route attaches 'auth' and 'oauth' groups to the given router.
-func (a *Auth) Route(r router.Router, m ...gin.HandlerFunc) {
+func (a *Auth) Route(r *router.Router, m ...gin.HandlerFunc) {
// create groupings for the 'auth' and 'oauth' prefixes
authGroup := r.AttachGroup("auth")
oauthGroup := r.AttachGroup("oauth")
diff --git a/internal/api/client.go b/internal/api/client.go
index ec8fa6034..1112efa31 100644
--- a/internal/api/client.go
+++ b/internal/api/client.go
@@ -79,7 +79,7 @@ type Client struct {
user *user.Module // api/v1/user
}
-func (c *Client) Route(r router.Router, m ...gin.HandlerFunc) {
+func (c *Client) Route(r *router.Router, m ...gin.HandlerFunc) {
// create a new group on the top level client 'api' prefix
apiGroup := r.AttachGroup("api")
diff --git a/internal/api/fileserver.go b/internal/api/fileserver.go
index 8f1e60b82..59f38c362 100644
--- a/internal/api/fileserver.go
+++ b/internal/api/fileserver.go
@@ -30,7 +30,7 @@ type Fileserver struct {
fileserver *fileserver.Module
}
-func (f *Fileserver) Route(r router.Router, m ...gin.HandlerFunc) {
+func (f *Fileserver) Route(r *router.Router, m ...gin.HandlerFunc) {
fileserverGroup := r.AttachGroup("fileserver")
// Attach middlewares appropriate for this group.
diff --git a/internal/api/nodeinfo.go b/internal/api/nodeinfo.go
index 22d314033..fb7918edc 100644
--- a/internal/api/nodeinfo.go
+++ b/internal/api/nodeinfo.go
@@ -29,7 +29,7 @@ type NodeInfo struct {
nodeInfo *nodeinfo.Module
}
-func (w *NodeInfo) Route(r router.Router, m ...gin.HandlerFunc) {
+func (w *NodeInfo) Route(r *router.Router, m ...gin.HandlerFunc) {
// group nodeinfo endpoints together
nodeInfoGroup := r.AttachGroup("nodeinfo")
diff --git a/internal/api/util/errorhandling.go b/internal/api/util/errorhandling.go
index 33f501474..4fa544ffd 100644
--- a/internal/api/util/errorhandling.go
+++ b/internal/api/util/errorhandling.go
@@ -19,6 +19,7 @@ package util
import (
"context"
+ "errors"
"net/http"
"codeberg.org/gruf/go-kv"
@@ -90,19 +91,40 @@ func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context) (
// try to serve an appropriate application/json content-type error.
// To override the default response type, specify `offers`.
//
-// If the requester already hung up on the request, ErrorHandler
-// will overwrite the given errWithCode with a 499 error to indicate
-// that the failure wasn't due to something we did, and will avoid
-// trying to write extensive bytes to the caller by just aborting.
+// If the requester already hung up on the request, or the server
+// timed out a very slow request, ErrorHandler will overwrite the
+// given errWithCode with a 408 or 499 error to indicate that the
+// failure wasn't due to something we did, and will avoid trying
+// to write extensive bytes to the caller by just aborting.
//
-// See: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#nginx.
-func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode), offers ...MIME) {
- if c.Request.Context().Err() != nil {
- // Context error means requester probably left already.
- // Wrap original error with a less alarming one. Then
- // we can return early, because it doesn't matter what
- // we send to the client at this point; they're gone.
- errWithCode = gtserror.NewErrorClientClosedRequest(errWithCode.Unwrap())
+// For 499, see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#nginx.
+func ErrorHandler(
+ c *gin.Context,
+ errWithCode gtserror.WithCode,
+ instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode),
+ offers ...MIME,
+) {
+ if ctxErr := c.Request.Context().Err(); ctxErr != nil {
+ // Context error means either client has left already,
+ // or server has timed out a very slow request.
+ //
+ // Rewrap the error with something less scary,
+ // and just abort the request gracelessly.
+ err := errWithCode.Unwrap()
+
+ if errors.Is(ctxErr, context.DeadlineExceeded) {
+ // We timed out the request.
+ errWithCode = gtserror.NewErrorRequestTimeout(err)
+
+ // Be correct and write "close".
+ // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Connection#close
+ // and: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408
+ c.Header("Connection", "close")
+ } else {
+ // Client timed out the request.
+ errWithCode = gtserror.NewErrorClientClosedRequest(err)
+ }
+
c.AbortWithStatus(errWithCode.Code())
return
}
diff --git a/internal/api/wellknown.go b/internal/api/wellknown.go
index 63ca48ef7..90e18d637 100644
--- a/internal/api/wellknown.go
+++ b/internal/api/wellknown.go
@@ -33,7 +33,7 @@ type WellKnown struct {
hostMeta *hostmeta.Module
}
-func (w *WellKnown) Route(r router.Router, m ...gin.HandlerFunc) {
+func (w *WellKnown) Route(r *router.Router, m ...gin.HandlerFunc) {
// group .well-known endpoints together
wellKnownGroup := r.AttachGroup(".well-known")
diff --git a/internal/gotosocial/gotosocial.go b/internal/gotosocial/gotosocial.go
index 74cc09f65..e6a3934be 100644
--- a/internal/gotosocial/gotosocial.go
+++ b/internal/gotosocial/gotosocial.go
@@ -29,7 +29,7 @@ import (
// GoToSocial server instance.
type Server struct {
db db.DB
- apiRouter router.Router
+ apiRouter *router.Router
cleaner *cleaner.Cleaner
}
@@ -37,7 +37,7 @@ type Server struct {
// GoToSocial server instance.
func NewServer(
db db.DB,
- apiRouter router.Router,
+ apiRouter *router.Router,
cleaner *cleaner.Cleaner,
) *Server {
return &Server{
diff --git a/internal/gtserror/withcode.go b/internal/gtserror/withcode.go
index 55fe7502a..d17a4e42e 100644
--- a/internal/gtserror/withcode.go
+++ b/internal/gtserror/withcode.go
@@ -198,3 +198,14 @@ func NewErrorClientClosedRequest(original error) WithCode {
code: StatusClientClosedRequest,
}
}
+
+// NewErrorRequestTimeout returns an ErrorWithCode 408 with the given original error.
+// This error type should only be used when the server has decided to hang up a client
+// request after x amount of time, to avoid keeping extremely slow client requests open.
+func NewErrorRequestTimeout(original error) WithCode {
+ return withCode{
+ original: original,
+ safe: errors.New(http.StatusText(http.StatusRequestTimeout)),
+ code: http.StatusRequestTimeout,
+ }
+}
diff --git a/internal/router/attach.go b/internal/router/attach.go
index 40e5992a4..4b4fd7ac4 100644
--- a/internal/router/attach.go
+++ b/internal/router/attach.go
@@ -19,18 +19,18 @@ package router
import "github.com/gin-gonic/gin"
-func (r *router) AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes {
+func (r *Router) AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes {
return r.engine.Use(handlers...)
}
-func (r *router) AttachNoRouteHandler(handler gin.HandlerFunc) {
+func (r *Router) AttachNoRouteHandler(handler gin.HandlerFunc) {
r.engine.NoRoute(handler)
}
-func (r *router) AttachGroup(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
+func (r *Router) AttachGroup(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
return r.engine.Group(relativePath, handlers...)
}
-func (r *router) AttachHandler(method string, path string, handler gin.HandlerFunc) {
+func (r *Router) AttachHandler(method string, path string, handler gin.HandlerFunc) {
r.engine.Handle(method, path, handler)
}
diff --git a/internal/router/router.go b/internal/router/router.go
index b6991f97f..f71dc97ef 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -29,6 +29,7 @@ import (
"codeberg.org/gruf/go-debug"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/log"
"golang.org/x/crypto/acme/autocert"
)
@@ -42,94 +43,120 @@ const (
maxMultipartMemory = int64(8 * bytesize.MiB)
)
-// Router provides the REST interface for gotosocial, using gin.
-type Router interface {
- // Attach global gin middlewares to this router.
- AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes
- // AttachGroup attaches the given handlers into a group with the given relativePath as
- // base path for that group. It then returns the *gin.RouterGroup so that the caller
- // can add any extra middlewares etc specific to that group, as desired.
- AttachGroup(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup
- // Attach a single gin handler to the router with the given method and path.
- // To make middleware management easier, AttachGroup should be preferred where possible.
- // However, this function can be used for attaching single handlers that only require
- // global middlewares.
- AttachHandler(method string, path string, handler gin.HandlerFunc)
-
- // Attach 404 NoRoute handler
- AttachNoRouteHandler(handler gin.HandlerFunc)
- // Start the router
- Start()
- // Stop the router
- Stop(ctx context.Context) error
+// Router provides the HTTP REST
+// interface for GoToSocial, using gin.
+type Router struct {
+ engine *gin.Engine
+ srv *http.Server
}
-// router fulfils the Router interface using gin and logrus
-type router struct {
- engine *gin.Engine
- srv *http.Server
- certManager *autocert.Manager
+// New returns a new Router, which wraps
+// an http server and gin handler engine.
+//
+// The router's Attach functions should be
+// used *before* the router is Started.
+//
+// When the router's work is finished, Stop
+// should be called on it to close connections
+// gracefully.
+//
+// The provided context will be used as the base
+// context for all requests passing through the
+// underlying http.Server, so this should be a
+// long-running context.
+func New(ctx context.Context) (*Router, error) {
+ // TODO: make this configurable?
+ gin.SetMode(gin.ReleaseMode)
+
+ // Create the engine here -- this is the core
+ // request routing handler for GoToSocial.
+ engine := gin.New()
+ engine.MaxMultipartMemory = maxMultipartMemory
+ engine.HandleMethodNotAllowed = true
+
+ // Set up client IP forwarding via
+ // trusted x-forwarded-* headers.
+ trustedProxies := config.GetTrustedProxies()
+ if err := engine.SetTrustedProxies(trustedProxies); err != nil {
+ return nil, err
+ }
+
+ // Attach functions used by HTML templating,
+ // and load HTML templates into the engine.
+ LoadTemplateFunctions(engine)
+ if err := LoadTemplates(engine); err != nil {
+ return nil, err
+ }
+
+ // Use the passed-in cmd context as the base context for the
+ // server, since we'll never want the server to live past the
+ // `server start` command anyway.
+ baseCtx := func(_ net.Listener) context.Context { return ctx }
+
+ addr := fmt.Sprintf("%s:%d",
+ config.GetBindAddress(),
+ config.GetPort(),
+ )
+
+ // Wrap the gin engine handler in our
+ // own timeout handler, to ensure we
+ // don't keep very slow requests around.
+ handler := timeoutHandler{engine}
+
+ s := &http.Server{
+ Addr: addr,
+ Handler: handler,
+ ReadTimeout: readTimeout,
+ ReadHeaderTimeout: readHeaderTimeout,
+ WriteTimeout: writeTimeout,
+ IdleTimeout: idleTimeout,
+ BaseContext: baseCtx,
+ }
+
+ return &Router{
+ engine: engine,
+ srv: s,
+ }, nil
}
-// Start starts the router nicely. It will serve two handlers if letsencrypt is enabled, and only the web/API handler if letsencrypt is not enabled.
-func (r *router) Start() {
- // listen is the server start function, by
- // default pointing to regular HTTP listener,
- // but updated to TLS if LetsEncrypt is enabled.
- listen := r.srv.ListenAndServe
-
- // During config validation we already checked that both Chain and Key are set
- // so we can forego checking for both here
- if chain := config.GetTLSCertificateChain(); chain != "" {
- pkey := config.GetTLSCertificateKey()
- cer, err := tls.LoadX509KeyPair(chain, pkey)
- if err != nil {
- log.Fatalf(
- nil,
- "tls: failed to load keypair from %s and %s, ensure they are PEM-encoded and can be read by this process: %s",
- chain, pkey, err,
- )
- }
- r.srv.TLSConfig = &tls.Config{
- MinVersion: tls.VersionTLS12,
- Certificates: []tls.Certificate{cer},
- }
- // TLS is enabled, update the listen function
- listen = func() error { return r.srv.ListenAndServeTLS("", "") }
+// Start starts the router nicely.
+//
+// It will serve two handlers if letsencrypt is enabled,
+// and only the web/API handler if letsencrypt is not enabled.
+func (r *Router) Start() {
+ var (
+ // listen is the server start function.
+ // By default this points to a regular
+ // HTTP listener, but will be changed to
+ // TLS if custom certs or LE are enabled.
+ listen func() error
+ err error
+
+ certFile = config.GetTLSCertificateChain()
+ keyFile = config.GetTLSCertificateKey()
+ leEnabled = config.GetLetsEncryptEnabled()
+ )
+
+ switch {
+ // TLS with custom certs.
+ case certFile != "":
+ // During config validation we already checked
+ // that either both or neither of Chain and Key
+ // are set, so we can forego checking again here.
+ listen, err = r.customTLS(certFile, keyFile)
+
+ // TLS with letsencrypt.
+ case leEnabled:
+ listen, err = r.letsEncryptTLS()
+
+ // Default listen. TLS must
+ // be handled by reverse proxy.
+ default:
+ listen = r.srv.ListenAndServe
}
- if config.GetLetsEncryptEnabled() {
- // LetsEncrypt support is enabled
-
- // Prepare an HTTPS-redirect handler for LetsEncrypt fallback
- redirect := http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
- target := "https://" + r.Host + r.URL.Path
- if len(r.URL.RawQuery) > 0 {
- target += "?" + r.URL.RawQuery
- }
- http.Redirect(rw, r, target, http.StatusTemporaryRedirect)
- })
-
- go func() {
- // Take our own copy of HTTP server
- // with updated autocert manager endpoint
- srv := (*r.srv) //nolint
- srv.Handler = r.certManager.HTTPHandler(redirect)
- srv.Addr = fmt.Sprintf("%s:%d",
- config.GetBindAddress(),
- config.GetLetsEncryptPort(),
- )
-
- // Start the LetsEncrypt autocert manager HTTP server.
- log.Infof(nil, "letsencrypt listening on %s", srv.Addr)
- if err := srv.ListenAndServe(); err != nil &&
- err != http.ErrServerClosed {
- log.Fatalf(nil, "letsencrypt: listen: %s", err)
- }
- }()
-
- // TLS is enabled, update the listen function
- listen = func() error { return r.srv.ListenAndServeTLS("", "") }
+ if err != nil {
+ log.Fatal(nil, err)
}
// Pass the server handler through a debug pprof middleware handler.
@@ -154,7 +181,7 @@ func (r *router) Start() {
}
// Stop shuts down the router nicely
-func (r *router) Stop(ctx context.Context) error {
+func (r *Router) Stop(ctx context.Context) error {
log.Infof(nil, "shutting down http router with %s grace period", shutdownTimeout)
timeout, cancel := context.WithTimeout(ctx, shutdownTimeout)
defer cancel()
@@ -167,78 +194,87 @@ func (r *router) Stop(ctx context.Context) error {
return nil
}
-// New returns a new Router.
-//
-// The router's Attach functions should be used *before* the router is Started.
-//
-// When the router's work is finished, Stop should be called on it to close connections gracefully.
-//
-// The provided context will be used as the base context for all requests passing
-// through the underlying http.Server, so this should be a long-running context.
-func New(ctx context.Context) (Router, error) {
- gin.SetMode(gin.TestMode)
-
- // create the actual engine here -- this is the core request routing handler for gts
- engine := gin.New()
- engine.MaxMultipartMemory = maxMultipartMemory
- engine.HandleMethodNotAllowed = true
-
- // set up IP forwarding via x-forward-* headers.
- trustedProxies := config.GetTrustedProxies()
- if err := engine.SetTrustedProxies(trustedProxies); err != nil {
+// customTLS modifies the router's underlying
+// http server to use custom TLS cert/key pair.
+func (r *Router) customTLS(
+ certFile string,
+ keyFile string,
+) (func() error, error) {
+ // Load certificates from disk.
+ cer, err := tls.LoadX509KeyPair(certFile, keyFile)
+ if err != nil {
+ err = gtserror.Newf(
+ "failed to load keypair from %s and %s, ensure they are "+
+ "PEM-encoded and can be read by this process: %w",
+ certFile, keyFile, err,
+ )
return nil, err
}
- // set template functions
- LoadTemplateFunctions(engine)
-
- // load templates onto the engine
- if err := LoadTemplates(engine); err != nil {
- return nil, err
+ // Override server's TLSConfig.
+ r.srv.TLSConfig = &tls.Config{
+ MinVersion: tls.VersionTLS12,
+ Certificates: []tls.Certificate{cer},
}
- // use the passed-in command context as the base context for the server,
- // since we'll never want the server to live past the command anyway
- baseCtx := func(_ net.Listener) context.Context {
- return ctx
+ // Update listen function to use custom TLS.
+ listen := func() error { return r.srv.ListenAndServeTLS("", "") }
+ return listen, nil
+}
+
+// letsEncryptTLS modifies the router's underlying http
+// server to use LetsEncrypt via an ACME Autocert manager.
+//
+// It also starts a listener on the configured LetsEncrypt
+// port to validate LE requests.
+func (r *Router) letsEncryptTLS() (func() error, error) {
+ acm := &autocert.Manager{
+ Prompt: autocert.AcceptTOS,
+ HostPolicy: autocert.HostWhitelist(config.GetHost()),
+ Cache: autocert.DirCache(config.GetLetsEncryptCertDir()),
+ Email: config.GetLetsEncryptEmailAddress(),
}
- bindAddress := config.GetBindAddress()
- port := config.GetPort()
- addr := fmt.Sprintf("%s:%d", bindAddress, port)
+ // Override server's TLSConfig.
+ r.srv.TLSConfig = acm.TLSConfig()
- s := &http.Server{
- Addr: addr,
- Handler: engine, // use gin engine as handler
- ReadTimeout: readTimeout,
- ReadHeaderTimeout: readHeaderTimeout,
- WriteTimeout: writeTimeout,
- IdleTimeout: idleTimeout,
- BaseContext: baseCtx,
+ // Prepare a fallback handler for LetsEncrypt.
+ //
+ // This will redirect all non-LetsEncrypt http
+ // reqs to https, preserving path and query params.
+ var fallback http.HandlerFunc = func(
+ w http.ResponseWriter,
+ r *http.Request,
+ ) {
+ // Rewrite target to https.
+ target := "https://" + r.Host + r.URL.Path
+ if len(r.URL.RawQuery) > 0 {
+ target += "?" + r.URL.RawQuery
+ }
+
+ http.Redirect(w, r, target, http.StatusTemporaryRedirect)
}
- // We need to spawn the underlying server slightly differently depending on whether lets encrypt is enabled or not.
- // In either case, the gin engine will still be used for routing requests.
- leEnabled := config.GetLetsEncryptEnabled()
-
- var m *autocert.Manager
- if leEnabled {
- // le IS enabled, so roll up an autocert manager for handling letsencrypt requests
- host := config.GetHost()
- leCertDir := config.GetLetsEncryptCertDir()
- leEmailAddress := config.GetLetsEncryptEmailAddress()
- m = &autocert.Manager{
- Prompt: autocert.AcceptTOS,
- HostPolicy: autocert.HostWhitelist(host),
- Cache: autocert.DirCache(leCertDir),
- Email: leEmailAddress,
+ // Take our own copy of the HTTP server,
+ // and update it to serve LetsEncrypt
+ // requests via the autocert manager.
+ leSrv := (*r.srv) //nolint:govet
+ leSrv.Handler = acm.HTTPHandler(fallback)
+ leSrv.Addr = fmt.Sprintf("%s:%d",
+ config.GetBindAddress(),
+ config.GetLetsEncryptPort(),
+ )
+
+ go func() {
+ // Start the LetsEncrypt autocert manager HTTP server.
+ log.Infof(nil, "letsencrypt listening on %s", leSrv.Addr)
+ if err := leSrv.ListenAndServe(); err != nil &&
+ err != http.ErrServerClosed {
+ log.Fatalf(nil, "letsencrypt: listen: %s", err)
}
- s.TLSConfig = m.TLSConfig()
- }
+ }()
- return &router{
- engine: engine,
- srv: s,
- certManager: m,
- }, nil
+ // Update listen function to use LetsEncrypt TLS.
+ listen := func() error { return r.srv.ListenAndServeTLS("", "") }
+ return listen, nil
}
diff --git a/internal/router/timeout.go b/internal/router/timeout.go
new file mode 100644
index 000000000..0f1ef869b
--- /dev/null
+++ b/internal/router/timeout.go
@@ -0,0 +1,61 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package router
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+)
+
+const requestTimeout = 10 * time.Minute
+
+type timeoutHandler struct {
+ *gin.Engine
+}
+
+// ServeHTTP wraps the embedded Gin engine's ServeHTTP
+// function with an injected context which times out
+// non-upgraded inbound requests after 10 minutes.
+func (th timeoutHandler) ServeHTTP(
+ w http.ResponseWriter,
+ r *http.Request,
+) {
+ if upgr := r.Header.Get("Upgrade"); upgr != "" {
+ // Upgrade to wss (probably).
+ // Leave well enough alone.
+ th.Engine.ServeHTTP(w, r)
+ return
+ }
+
+ // Create timeout ctx.
+ toCtx, cancelCtx := context.WithTimeout(
+ r.Context(),
+ requestTimeout,
+ )
+ defer cancelCtx()
+
+ // Serve the request using a shallow copy
+ // with the new context, without replacing
+ // the underlying request, since the latter
+ // may be used later outside of the Gin
+ // engine for post-request cleanup tasks.
+ th.Engine.ServeHTTP(w, r.WithContext(toCtx))
+}
diff --git a/internal/web/web.go b/internal/web/web.go
index 6d785667b..86e74d6f8 100644
--- a/internal/web/web.go
+++ b/internal/web/web.go
@@ -74,7 +74,7 @@ func New(db db.DB, processor *processing.Processor) *Module {
}
}
-func (m *Module) Route(r router.Router, mi ...gin.HandlerFunc) {
+func (m *Module) Route(r *router.Router, mi ...gin.HandlerFunc) {
// Group all static files from assets dir at /assets,
// so that they can use the same cache control middleware.
webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir())
diff --git a/testrig/router.go b/testrig/router.go
index 42716da29..64b3842de 100644
--- a/testrig/router.go
+++ b/testrig/router.go
@@ -33,7 +33,7 @@ import (
//
// If the environment variable GTS_WEB_TEMPLATE_BASE_DIR set, it will take that
// value as the template base directory instead.
-func NewTestRouter(db db.DB) router.Router {
+func NewTestRouter(db db.DB) *router.Router {
if alternativeTemplateBaseDir := os.Getenv("GTS_WEB_TEMPLATE_BASE_DIR"); alternativeTemplateBaseDir != "" {
config.Config(func(cfg *config.Configuration) {
cfg.WebTemplateBaseDir = alternativeTemplateBaseDir