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) 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 util
import (
"context"
"net/http"
"codeberg.org/gruf/go-kv"
"github.com/gin-gonic/gin"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/log"
)
// 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) (*apimodel.InstanceV1, gtserror.WithCode), accept string) {
switch accept {
case string(TextHTML):
ctx := c.Request.Context()
instance, err := instanceGet(ctx)
if err != nil {
panic(err)
}
c.HTML(http.StatusNotFound, "404.tmpl", gin.H{
"instance": instance,
"requestID": gtscontext.RequestID(ctx),
})
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) (*apimodel.InstanceV1, gtserror.WithCode), accept string, errWithCode gtserror.WithCode) {
switch accept {
case string(TextHTML):
ctx := c.Request.Context()
instance, err := instanceGet(ctx)
if err != nil {
panic(err)
}
c.HTML(errWithCode.Code(), "error.tmpl", gin.H{
"instance": instance,
"code": errWithCode.Code(),
"error": errWithCode.Safe(),
"requestID": gtscontext.RequestID(ctx),
})
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.
// 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.
//
// 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())
c.AbortWithStatus(errWithCode.Code())
return
}
// Set the error on the gin context so that it can be logged
// in the gin logger middleware (internal/middleware/logger.go).
c.Error(errWithCode) //nolint:errcheck
// 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).
// Prefer provided offers, fall back to JSON or HTML.
accept, _ := NegotiateAccept(c, append(offers, JSONOrHTMLAcceptHeaders...)...)
if errWithCode.Code() == http.StatusNotFound {
// Use our special not found handler with useful status text.
NotFoundHandler(c, instanceGet, accept)
} else {
genericErrorHandler(c, instanceGet, accept, errWithCode)
}
}
// WebErrorHandler is like ErrorHandler, but will display HTML over JSON by default.
func WebErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode)) {
ErrorHandler(c, errWithCode, instanceGet, TextHTML, AppJSON)
}
// 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 := log.WithContext(c.Request.Context()).
WithFields(kv.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(),
})
}
|