summaryrefslogtreecommitdiff
path: root/workhorse/internal/badgateway/roundtripper.go
blob: ce4e9e6a177d91a754174513323147f965aa9fb2 (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
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>
`))