summaryrefslogtreecommitdiff
path: root/internal/api/errorhandling.go
blob: 834f49ee8c67603cd3779f672c253b48e45e8e41 (plain)
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
/*
   GoToSocial
   Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org

   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 api

import (
	"context"
	"net/http"

	"codeberg.org/gruf/go-errors/v2"
	"github.com/gin-gonic/gin"
	"github.com/sirupsen/logrus"
	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
	"github.com/superseriousbusiness/gotosocial/internal/config"
	"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)

// TODO: add more templated html pages here for different error types

// NotFoundHandler serves a 404 html page through the provided gin context,
// if accept is 'text/html', or just returns a json error if 'accept' is empty
// or application/json.
//
// When serving html, NotFoundHandler calls the provided InstanceGet function
// to fetch the apimodel representation of the instance, for serving in the
// 404 header and footer.
//
// If an error is returned by InstanceGet, the function will panic.
func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string) {
	switch accept {
	case string(TextHTML):
		host := config.GetHost()
		instance, err := instanceGet(c.Request.Context(), host)
		if err != nil {
			panic(err)
		}

		c.HTML(http.StatusNotFound, "404.tmpl", gin.H{
			"instance": instance,
		})
	default:
		c.JSON(http.StatusNotFound, gin.H{"error": http.StatusText(http.StatusNotFound)})
	}
}

// genericErrorHandler is a more general version of the NotFoundHandler, which can
// be used for serving either generic error pages with some rendered help text,
// or just some error json if the caller prefers (or has no preference).
func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string, errWithCode gtserror.WithCode) {
	switch accept {
	case string(TextHTML):
		host := config.GetHost()
		instance, err := instanceGet(c.Request.Context(), host)
		if err != nil {
			panic(err)
		}

		c.HTML(errWithCode.Code(), "error.tmpl", gin.H{
			"instance": instance,
			"code":     errWithCode.Code(),
			"error":    errWithCode.Safe(),
		})
	default:
		c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
	}
}

// ErrorHandler takes the provided gin context and errWithCode and tries to serve
// a helpful error to the caller. It will do content negotiation to figure out if
// the caller prefers to see an html page with the error rendered there. If not, or
// if something goes wrong during the function, it will recover and just try to serve
// an appropriate application/json content-type error.
func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode)) {
	path := c.Request.URL.Path
	if raw := c.Request.URL.RawQuery; raw != "" {
		path = path + "?" + raw
	}

	l := logrus.WithFields(logrus.Fields{
		"path":  path,
		"error": errWithCode.Error(),
	})

	statusCode := errWithCode.Code()

	if statusCode == http.StatusInternalServerError {
		l.Error("Internal Server Error")
	} else {
		l.Debug("handling error")
	}

	// if we panic for any reason during error handling,
	// we should still try to return a basic code
	defer func() {
		if p := recover(); p != nil {
			// Fetch stacktrace up to this point
			callers := errors.GetCallers(3, 10)

			// Log this panic to the standard log
			l = l.WithField("stacktrace", callers)
			l.Errorf("recovered from panic: %v", p)

			// Respond with determined error code
			c.JSON(statusCode, gin.H{"error": errWithCode.Safe()})
		}
	}()

	// discover if we're allowed to serve a nice html error page,
	// or if we should just use a json. Normally we would want to
	// check for a returned error, but if an error occurs here we
	// can just fall back to default behavior (serve json error).
	accept, _ := NegotiateAccept(c, HTMLOrJSONAcceptHeaders...)

	if statusCode == http.StatusNotFound {
		// use our special not found handler with useful status text
		NotFoundHandler(c, instanceGet, accept)
	} else {
		genericErrorHandler(c, instanceGet, accept, errWithCode)
	}
}

// OAuthErrorHandler is a lot like ErrorHandler, but it specifically returns errors
// that are compatible with https://datatracker.ietf.org/doc/html/rfc6749#section-5.2,
// but serializing errWithCode.Error() in the 'error' field, and putting any help text
// from the error in the 'error_description' field. This means you should be careful not
// to pass any detailed errors (that might contain sensitive information) into the
// errWithCode.Error() field, since the client will see this. Use your noggin!
func OAuthErrorHandler(c *gin.Context, errWithCode gtserror.WithCode) {
	l := logrus.WithFields(logrus.Fields{
		"path":  c.Request.URL.Path,
		"error": errWithCode.Error(),
		"help":  errWithCode.Safe(),
	})

	statusCode := errWithCode.Code()

	if statusCode == http.StatusInternalServerError {
		l.Error("Internal Server Error")
	} else {
		l.Debug("handling OAuth error")
	}

	c.JSON(statusCode, gin.H{
		"error":             errWithCode.Error(),
		"error_description": errWithCode.Safe(),
	})
}