• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

foomo / contentserver / 25115160466

29 Apr 2026 02:34PM UTC coverage: 43.419% (+1.7%) from 41.687%
25115160466

push

github

web-flow
Merge pull request #76 from foomo/feature/etag-conditional-poll-and-go-1.26-bump

ETag-aware conditional poll + Go 1.26 bump

24 of 32 new or added lines in 3 files covered. (75.0%)

48 existing lines in 2 files now uncovered.

927 of 2135 relevant lines covered (43.42%)

22677.39 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/pkg/handler/http.go
1
package handler
2

3
import (
4
        "context"
5
        "io"
6
        "net/http"
7
        "strings"
8
        "time"
9

10
        "github.com/foomo/contentserver/pkg/metrics"
11
        "github.com/foomo/contentserver/pkg/repo"
12
        "github.com/foomo/contentserver/requests"
13
        "github.com/foomo/contentserver/responses"
14
        httputils "github.com/foomo/keel/utils/net/http"
15
        "github.com/pkg/errors"
16
        "go.uber.org/zap"
17
)
18

19
type (
20
        HTTP struct {
21
                l        *zap.Logger
22
                repo     *repo.Repo
23
                basePath string
24
        }
25
        HTTPOption func(*HTTP)
26
)
27

28
// ------------------------------------------------------------------------------------------------
29
// ~ Constructor
30
// ------------------------------------------------------------------------------------------------
31

32
// NewHTTP returns a shiny new web server
33
func NewHTTP(l *zap.Logger, repo *repo.Repo, opts ...HTTPOption) http.Handler {
×
34
        inst := &HTTP{
×
35
                l:        l.Named("http"),
×
36
                basePath: "/contentserver",
×
37
                repo:     repo,
×
38
        }
×
39

×
40
        for _, opt := range opts {
×
41
                opt(inst)
×
42
        }
×
43

44
        return inst
×
45
}
46

47
// ------------------------------------------------------------------------------------------------
48
// ~ Options
49
// ------------------------------------------------------------------------------------------------
50

51
func WithBasePath(v string) HTTPOption {
×
52
        return func(o *HTTP) {
×
53
                o.basePath = v
×
54
        }
×
55
}
56

57
// ------------------------------------------------------------------------------------------------
58
// ~ Public methods
59
// ------------------------------------------------------------------------------------------------
60

61
func (h *HTTP) ServeHTTP(w http.ResponseWriter, r *http.Request) {
×
62
        if r.Method != http.MethodPost {
×
63
                httputils.ServerError(h.l, w, r, http.StatusMethodNotAllowed, errors.New("method not allowed"))
×
64
                return
×
65
        }
×
66
        if r.Body == nil {
×
67
                httputils.BadRequestServerError(h.l, w, r, errors.New("empty request body"))
×
68
                return
×
69
        }
×
70

71
        bytes, err := io.ReadAll(r.Body)
×
72
        if err != nil {
×
73
                httputils.BadRequestServerError(h.l, w, r, errors.Wrap(err, "failed to read incoming request"))
×
74
                return
×
75
        }
×
76

77
        route := Route(strings.TrimPrefix(r.URL.Path, h.basePath+"/"))
×
78
        if route == RouteGetRepo {
×
79
                w.Header().Set("Content-Type", "application/json")
×
80
                if err := h.repo.WriteRepoBytes(r.Context(), w); err != nil {
×
81
                        h.l.Error("failed to write repo bytes", zap.Error(err))
×
82
                        http.Error(w, "failed to get repo", http.StatusInternalServerError)
×
83
                }
×
84
                return
×
85
        }
86

87
        reply, errReply := h.handleRequest(r.Context(), h.repo, route, bytes, "webserver")
×
88
        if errReply != nil {
×
89
                http.Error(w, errReply.Error(), http.StatusInternalServerError)
×
90
                return
×
91
        }
×
NEW
92
        w.Header().Set("Content-Type", "application/json")
×
NEW
93
        w.Header().Set("X-Content-Type-Options", "nosniff")
×
NEW
94
        // reply is produced by encodeReply -> json.Marshal (jsoniter ConfigCompatibleWithStandardLibrary),
×
NEW
95
        // which HTML-escapes <, >, & by default. Combined with the explicit JSON content type and the
×
NEW
96
        // nosniff header above, this is safe to write back to the client. gosec G705 cannot follow taint
×
NEW
97
        // through the third-party JSON encoder and reports a false positive.
×
NEW
98
        _, _ = w.Write(reply) //nolint:gosec // see comment above
×
99
}
100

101
// ------------------------------------------------------------------------------------------------
102
// ~ Private methods
103
// ------------------------------------------------------------------------------------------------
104

105
func (h *HTTP) handleRequest(ctx context.Context, r *repo.Repo, route Route, jsonBytes []byte, source string) ([]byte, error) {
×
106
        start := time.Now()
×
107

×
108
        reply, err := h.executeRequest(ctx, r, route, jsonBytes, source)
×
109
        result := "success"
×
110
        if err != nil {
×
111
                result = "error"
×
112
        }
×
113

114
        metrics.ServiceRequestCounter.WithLabelValues(string(route), result, source).Inc()
×
115
        metrics.ServiceRequestDuration.WithLabelValues(string(route), result, source).Observe(time.Since(start).Seconds())
×
116

×
117
        return reply, err
×
118
}
119

120
func (h *HTTP) executeRequest(ctx context.Context, r *repo.Repo, route Route, jsonBytes []byte, source string) (replyBytes []byte, err error) {
×
121
        var (
×
122
                reply             interface{}
×
123
                apiErr            error
×
124
                jsonErr           error
×
125
                processIfJSONIsOk = func(err error, processingFunc func()) {
×
126
                        if err != nil {
×
127
                                jsonErr = err
×
128
                                return
×
129
                        }
×
130
                        processingFunc()
×
131
                }
132
        )
133
        metrics.ContentRequestCounter.WithLabelValues(source).Inc()
×
134

×
135
        // handle and process
×
136
        switch route {
×
137
        // case HandlerGetRepo: // This case is handled prior to handleRequest being called.
138
        // since the resulting bytes are written directly in to the http.ResponseWriter / net.Connection
139
        case RouteGetURIs:
×
140
                getURIRequest := &requests.URIs{}
×
141
                processIfJSONIsOk(json.Unmarshal(jsonBytes, &getURIRequest), func() {
×
142
                        reply = r.GetURIs(getURIRequest.Dimension, getURIRequest.IDs)
×
143
                })
×
144
        case RouteGetContent:
×
145
                contentRequest := &requests.Content{}
×
146
                processIfJSONIsOk(json.Unmarshal(jsonBytes, &contentRequest), func() {
×
147
                        reply, apiErr = r.GetContent(contentRequest)
×
148
                })
×
149
        case RouteGetNodes:
×
150
                nodesRequest := &requests.Nodes{}
×
151
                processIfJSONIsOk(json.Unmarshal(jsonBytes, &nodesRequest), func() {
×
152
                        reply = r.GetNodes(nodesRequest)
×
153
                })
×
154
        case RouteUpdate:
×
155
                updateRequest := &requests.Update{}
×
156
                processIfJSONIsOk(json.Unmarshal(jsonBytes, &updateRequest), func() {
×
157
                        reply = r.Update(ctx)
×
158
                })
×
159
        default:
×
160
                reply = responses.NewError(1, "unknown route: "+string(route))
×
161
        }
162

163
        // error handling
164
        if jsonErr != nil {
×
165
                h.l.Error("could not read incoming json", zap.Error(jsonErr))
×
166
                reply = responses.NewError(2, "could not read incoming json "+jsonErr.Error())
×
167
        } else if apiErr != nil {
×
168
                h.l.Error("an API error occurred", zap.Error(apiErr))
×
169
                reply = responses.NewError(3, "internal error "+apiErr.Error())
×
170
        }
×
171

172
        return h.encodeReply(reply)
×
173
}
174

175
// encodeReply takes an interface and encodes it as JSON
176
// it returns the resulting JSON and a marshalling error
177
func (h *HTTP) encodeReply(reply interface{}) (bytes []byte, err error) {
×
178
        bytes, err = json.Marshal(map[string]interface{}{
×
179
                "reply": reply,
×
180
        })
×
181
        if err != nil {
×
182
                h.l.Error("could not encode reply", zap.Error(err))
×
183
        }
×
184
        return
×
185
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc