summaryrefslogtreecommitdiff
path: root/workhorse/internal/badgateway/roundtripper.go
blob: 86337e80f285ca1d4991f2f8b17d29d46f584c80 (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
package badgateway

import (
	"bytes"
	"fmt"
	"html/template"
	"io/ioutil"
	"net/http"
	"strings"
	"time"

	"gitlab.com/gitlab-org/gitlab/workhorse/internal/log"
)

// 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()

	injectedResponse := &http.Response{
		StatusCode: http.StatusBadGateway,
		Status:     http.StatusText(http.StatusBadGateway),

		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 = ioutil.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,
	}

	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
}

var developmentErrorTemplate = template.Must(template.New("error502").Parse(`
<html>
<head>
<title>502: GitLab is not responding</title>
<script>
window.setTimeout(function() { location.reload() }, {{.ReloadSeconds}} * 1000)
</script>
</head>

<body>
<h1>502</h1>
<p>GitLab is not responding. The error was:</p>

<pre>{{.Error}}</pre>

<p>If you just started GDK it can take 60-300 seconds before GitLab has finished booting. This page will automatically reload every {{.ReloadSeconds}} seconds.</p>
<footer>Generated by gitlab-workhorse running in development mode at {{.Time}}.</footer>
</body>
</html>
`))