debian-mirror-gitlab/workhorse/internal/badgateway/roundtripper.go
2023-03-17 16:20:25 +05:30

137 lines
3.5 KiB
Go

package badgateway
import (
"bytes"
"context"
_ "embed"
"encoding/base64"
"fmt"
"html/template"
"io"
"net/http"
"strings"
"time"
"gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
)
//go:embed embed/gitlab-logo-500.png
var gitlabLogo []byte
// Error is a custom error for pretty Sentry 'issues'
type sentryError struct{ error }
type roundTripper struct {
next http.RoundTripper
developmentMode bool
}
// NewRoundTripper creates a RoundTripper with a provided underlying next transport
func NewRoundTripper(developmentMode bool, next http.RoundTripper) http.RoundTripper {
return &roundTripper{next: next, developmentMode: developmentMode}
}
func (t *roundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
start := time.Now()
res, err := t.next.RoundTrip(r)
if err == nil {
return res, err
}
// httputil.ReverseProxy translates all errors from this
// RoundTrip function into 500 errors. But the most likely error
// is that the Rails app is not responding, in which case users
// and administrators expect to see a 502 error. To show 502s
// instead of 500s we catch the RoundTrip error here and inject a
// 502 response.
fields := log.Fields{"duration_ms": int64(time.Since(start).Seconds() * 1000)}
log.WithRequest(r).WithFields(fields).WithError(&sentryError{fmt.Errorf("badgateway: failed to receive response: %v", err)}).Error()
code := http.StatusBadGateway
if r.Context().Err() == context.Canceled {
code = 499 // Code used by NGINX when client disconnects
}
injectedResponse := &http.Response{
StatusCode: code,
Status: http.StatusText(code),
Request: r,
ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor,
Proto: r.Proto,
Header: make(http.Header),
Trailer: make(http.Header),
}
message := "GitLab is not responding"
contentType := "text/plain"
if t.developmentMode {
message, contentType = developmentModeResponse(err)
}
injectedResponse.Body = io.NopCloser(strings.NewReader(message))
injectedResponse.Header.Set("Content-Type", contentType)
return injectedResponse, nil
}
func developmentModeResponse(err error) (body string, contentType string) {
data := TemplateData{
Time: time.Now().Format("15:04:05"),
Error: err.Error(),
ReloadSeconds: 5,
Base64EncodedGitLabLogo: base64.StdEncoding.EncodeToString(gitlabLogo),
}
buf := &bytes.Buffer{}
if err := developmentErrorTemplate.Execute(buf, data); err != nil {
return data.Error, "text/plain"
}
return buf.String(), "text/html"
}
type TemplateData struct {
Time string
Error string
ReloadSeconds int
Base64EncodedGitLabLogo string
}
var developmentErrorTemplate = template.Must(template.New("error502").Parse(`
<!DOCTYPE html>
<html lang="en">
<head>
<title>Waiting for GitLab to boot</title>
</head>
<body>
<div style="text-align: center; font-family: Source Sans Pro, sans-serif">
<img style="padding: 60px 0" src="data:image/png;base64,{{.Base64EncodedGitLabLogo}}" alt="GitLab" />
<h1>Waiting for GitLab to boot</h1>
<pre>{{.Error}}</pre>
<br/>
<p>It can take 60-180 seconds for GitLab to boot completely.</p>
<p>This page will automatically reload every {{.ReloadSeconds}} seconds.</p>
<br/>
<footer>
<p>Generated by gitlab-workhorse running in development mode at {{.Time}}.</p>
</footer>
</div>
<script>
window.setTimeout(function() { location.reload(); }, {{.ReloadSeconds}} * 1000)
</script>
</body>
</html>
`))