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

resgateio / resgate / 25635959050

10 May 2026 06:04PM UTC coverage: 90.397% (-0.06%) from 90.452%
25635959050

Pull #268

github

jirenius
GH-267: Updated how responses to CORS requests are made for when `allowOrigin` is *.
Removed CORS headers in responses with missing Origin, Origin:null, or disallowed origin.
Pull Request #268: GH-267: Updated how responses to CORS requests are made for when `allowOrigin` is *.

18 of 18 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

3756 of 4155 relevant lines covered (90.4%)

774.33 hits per line

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

95.09
/server/apiHandler.go
1
package server
2

3
import (
4
        "encoding/json"
5
        "fmt"
6
        "io"
7
        "mime"
8
        "net/http"
9
        "strings"
10

11
        "github.com/resgateio/resgate/server/codec"
12
        "github.com/resgateio/resgate/server/reserr"
13
)
14

15
func (s *Service) initAPIHandler() error {
1,118✔
16
        f := apiEncoderFactories[strings.ToLower(s.cfg.APIEncoding)]
1,118✔
17
        if f == nil {
1,119✔
18
                keys := make([]string, 0, len(apiEncoderFactories))
1✔
19
                for k := range apiEncoderFactories {
3✔
20
                        keys = append(keys, k)
2✔
21
                }
2✔
22
                return fmt.Errorf("invalid apiEncoding setting (%s) - available encodings: %s", s.cfg.APIEncoding, strings.Join(keys, ", "))
1✔
23
        }
24
        s.enc = f(s.cfg)
1,117✔
25
        mimetype, _, err := mime.ParseMediaType(s.enc.ContentType())
1,117✔
26
        s.mimetype = mimetype
1,117✔
27
        return err
1,117✔
28
}
29

30
// setCommonHeaders sets common headers such as Access-Control-*.
31
func (s *Service) setCommonHeaders(w http.ResponseWriter, r *http.Request) {
297✔
32
        origin := r.Header.Get("Origin")
297✔
33
        if origin == "" || origin == "null" {
499✔
34
                return
202✔
35
        }
202✔
36

37
        isWild := s.cfg.allowOrigin[0] == "*"
95✔
38
        allowCredentials := s.cfg.HeaderAuth != nil
95✔
39

95✔
40
        if allowCredentials {
145✔
41
                w.Header().Set("Access-Control-Allow-Credentials", "true")
50✔
42
        }
50✔
43

44
        if isWild {
123✔
45
                if allowCredentials {
53✔
46
                        w.Header().Set("Access-Control-Allow-Origin", origin)
25✔
47
                        w.Header().Set("Vary", "Origin")
25✔
48
                } else {
28✔
49
                        w.Header().Set("Access-Control-Allow-Origin", "*")
3✔
50
                }
3✔
51
                return
28✔
52
        }
53

54
        if matchesOrigins(s.cfg.allowOrigin, origin) {
131✔
55
                w.Header().Set("Access-Control-Allow-Origin", origin)
64✔
56
                w.Header().Set("Vary", "Origin")
64✔
57
        }
64✔
58
}
59

60
func (s *Service) apiHandler(w http.ResponseWriter, r *http.Request) {
297✔
61
        s.setCommonHeaders(w, r)
297✔
62
        if r.Method == "OPTIONS" {
308✔
63
                w.Header().Set("Access-Control-Allow-Methods", s.cfg.allowMethods)
11✔
64
                reqHeaders := r.Header["Access-Control-Request-Headers"]
11✔
65
                if len(reqHeaders) > 0 {
14✔
66
                        w.Header().Set("Access-Control-Allow-Headers", strings.Join(reqHeaders, ", "))
3✔
67
                }
3✔
68
                return
11✔
69
        }
70

71
        path := r.URL.RawPath
286✔
72
        if path == "" {
567✔
73
                path = r.URL.Path
281✔
74
        }
281✔
75

76
        apiPath := s.cfg.APIPath
286✔
77

286✔
78
        // NotFound on paths with trailing slash (unless it is only the APIPath)
286✔
79
        if len(path) > len(apiPath) && path[len(path)-1] == '/' {
290✔
80
                notFoundHandler(w, s.enc)
4✔
81
                return
4✔
82
        }
4✔
83

84
        var rid, action string
282✔
85
        switch r.Method {
282✔
86
        case "HEAD":
6✔
87
                fallthrough
6✔
88
        case "GET":
152✔
89
                // Metrics
152✔
90
                if s.metrics != nil {
153✔
91
                        s.metrics.HTTPRequestsGet.Add(1)
1✔
92
                }
1✔
93

94
                rid = PathToRID(path, r.URL.RawQuery, apiPath)
152✔
95
                if !codec.IsValidRID(rid, true) {
164✔
96
                        notFoundHandler(w, s.enc)
12✔
97
                        return
12✔
98
                }
12✔
99

100
                s.temporaryConn(w, r, func(c *wsConn, cb func([]byte, string, error, *codec.Meta)) {
253✔
101
                        c.GetHTTPSubscription(rid, func(sub *Subscription, meta *codec.Meta, err error) {
226✔
102
                                var b []byte
113✔
103
                                if err == nil && !meta.IsDirectResponseStatus() {
218✔
104
                                        b, err = s.enc.EncodeGET(sub)
105✔
105
                                }
105✔
106
                                cb(b, "", err, meta)
113✔
107
                        })
108
                })
109
                return
140✔
110

111
        case "POST":
109✔
112
                // Metrics
109✔
113
                if s.metrics != nil {
110✔
114
                        s.metrics.HTTPRequestsPost.Add(1)
1✔
115
                }
1✔
116

117
                rid, action = PathToRIDAction(path, r.URL.RawQuery, apiPath)
109✔
118
        default:
21✔
119
                var m *string
21✔
120
                switch r.Method {
21✔
121
                case "PUT":
7✔
122
                        if s.cfg.PUTMethod != nil {
13✔
123
                                m = s.cfg.PUTMethod
6✔
124
                        }
6✔
125
                case "DELETE":
7✔
126
                        if s.cfg.DELETEMethod != nil {
13✔
127
                                m = s.cfg.DELETEMethod
6✔
128
                        }
6✔
129
                case "PATCH":
7✔
130
                        if s.cfg.PATCHMethod != nil {
13✔
131
                                m = s.cfg.PATCHMethod
6✔
132
                        }
6✔
133
                }
134
                // Return error if we have no mapping for the method
135
                if m == nil {
24✔
136
                        httpError(w, reserr.ErrMethodNotAllowed, s.enc)
3✔
137
                        return
3✔
138
                }
3✔
139

140
                // Metrics
141
                if s.metrics != nil {
21✔
142
                        s.metrics.HTTPRequests.With(r.Method).Add(1)
3✔
143
                }
3✔
144

145
                rid = PathToRID(path, r.URL.RawQuery, apiPath)
18✔
146
                action = *m
18✔
147
        }
148

149
        s.handleCall(w, r, rid, action)
127✔
150
}
151

152
func notFoundHandler(w http.ResponseWriter, enc APIEncoder) {
26✔
153
        w.Header().Set("Content-Type", enc.ContentType())
26✔
154
        w.WriteHeader(http.StatusNotFound)
26✔
155
        w.Write(enc.NotFoundError())
26✔
156
}
26✔
157

158
func (s *Service) handleCall(w http.ResponseWriter, r *http.Request, rid string, action string) {
127✔
159
        if !codec.IsValidRID(rid, true) || !codec.IsValidRIDPart(action) {
133✔
160
                notFoundHandler(w, s.enc)
6✔
161
                return
6✔
162
        }
6✔
163

164
        // Try to parse the body
165
        b, err := io.ReadAll(r.Body)
121✔
166
        if err != nil {
121✔
167
                httpError(w, &reserr.Error{Code: reserr.CodeBadRequest, Message: "Error reading request body: " + err.Error()}, s.enc)
×
168
                return
×
169
        }
×
170

171
        var params json.RawMessage
121✔
172
        if strings.TrimSpace(string(b)) != "" {
147✔
173
                err = json.Unmarshal(b, &params)
26✔
174
                if err != nil {
26✔
175
                        httpError(w, &reserr.Error{Code: reserr.CodeBadRequest, Message: "Error decoding request body: " + err.Error()}, s.enc)
×
176
                        return
×
177
                }
×
178
        }
179

180
        s.temporaryConn(w, r, func(c *wsConn, cb func([]byte, string, error, *codec.Meta)) {
242✔
181
                c.CallHTTPResource(rid, action, params, func(r json.RawMessage, refRID string, err error, meta *codec.Meta) {
242✔
182
                        var b []byte
121✔
183
                        if err == nil && refRID == "" && !meta.IsDirectResponseStatus() {
179✔
184
                                b, err = s.enc.EncodePOST(r)
58✔
185
                        }
58✔
186
                        cb(b, RIDToPath(refRID, s.cfg.APIPath), err, meta)
121✔
187
                })
188
        })
189
}
190

191
// temporaryConn creates a temporary connection which is provides through a
192
// callback. Once the callback calls its write callback, the temporaryConn will
193
// return.
194
// The write callback takes 4 arguments, with priority in order: meta, err, href, out
195
// * out  - If not nil, it will be the output with a status OK response
196
// * href - If not empty, it will be used as Location for a Found response
197
// * err  - If not empty, it will be encoded into an error for an error response based on the error code
198
// * meta - If not empty, may change the behavior of all the others.
199
func (s *Service) temporaryConn(w http.ResponseWriter, r *http.Request, cb func(*wsConn, func(out []byte, href string, err error, meta *codec.Meta))) {
261✔
200
        c := s.newWSConn(r, versionLatest)
261✔
201
        if c == nil {
261✔
202
                httpError(w, reserr.ErrServiceUnavailable, s.enc)
×
203
                return
×
204
        }
×
205

206
        done := make(chan struct{})
261✔
207
        var authMeta *codec.Meta
261✔
208
        rs := func(out []byte, href string, err error, meta *codec.Meta) {
495✔
209
                defer c.dispose()
234✔
210
                defer close(done)
234✔
211

234✔
212
                // Merge auth meta into the callbacks meta
234✔
213
                meta = authMeta.Merge(meta)
234✔
214

234✔
215
                // Validate the status of the meta object.
234✔
216
                if !meta.IsValidStatus() {
242✔
217
                        s.Errorf("Invalid meta status: %d", *meta.Status)
8✔
218
                        meta.Status = nil
8✔
219
                }
8✔
220

221
                // Handle meta override
222
                if meta.IsDirectResponseStatus() {
251✔
223
                        httpStatusResponse(w, s.enc, *meta.Status, meta.Header, href, err)
17✔
224
                        return
17✔
225
                }
17✔
226

227
                // Merge any meta headers into the response header.
228
                codec.MergeHeader(w.Header(), meta.GetHeader())
217✔
229

217✔
230
                // Handle error
217✔
231
                if err != nil {
264✔
232
                        // Convert system.methodNotFound to system.methodNotAllowed for PUT/DELETE/PATCH
47✔
233
                        if rerr, ok := err.(*reserr.Error); ok {
94✔
234
                                if rerr.Code == reserr.CodeMethodNotFound && (r.Method == "PUT" || r.Method == "DELETE" || r.Method == "PATCH") {
50✔
235
                                        err = reserr.ErrMethodNotAllowed
3✔
236
                                }
3✔
237
                        }
238
                        httpError(w, err, s.enc)
47✔
239
                        return
47✔
240
                }
241

242
                // Handle href
243
                if href != "" {
177✔
244
                        w.Header().Set("Location", href)
7✔
245
                        w.WriteHeader(http.StatusOK)
7✔
246
                        return
7✔
247
                }
7✔
248

249
                // Output content
250
                if len(out) > 0 {
300✔
251
                        w.Header().Set("Content-Type", s.enc.ContentType())
137✔
252
                        w.Write(out)
137✔
253
                        return
137✔
254
                }
137✔
255

256
                // No content
257
                w.WriteHeader(http.StatusNoContent)
26✔
258
        }
259
        c.Enqueue(func() {
522✔
260
                if s.cfg.HeaderAuth != nil {
332✔
261
                        c.AuthResourceNoResult(s.cfg.headerAuthRID, s.cfg.headerAuthAction, nil, func(refRID string, err error, m *codec.Meta) {
142✔
262
                                if m.IsDirectResponseStatus() {
98✔
263
                                        httpStatusResponse(w, s.enc, *m.Status, m.Header, RIDToPath(refRID, s.cfg.APIPath), err)
27✔
264
                                        c.dispose()
27✔
265
                                        close(done)
27✔
266
                                        return
27✔
267
                                }
27✔
268

269
                                authMeta = m
44✔
270
                                cb(c, rs)
44✔
271
                        })
272
                } else {
190✔
273
                        cb(c, rs)
190✔
274
                }
190✔
275
        })
276
        <-done
261✔
277
}
278

279
func httpStatusResponse(w http.ResponseWriter, enc APIEncoder, status int, header http.Header, href string, err error) {
46✔
280
        // Redirect
46✔
281
        if status >= 300 && status < 400 {
54✔
282
                if href != "" {
10✔
283
                        if _, ok := header["Location"]; !ok {
4✔
284
                                w.Header().Set("Location", href)
2✔
285
                        }
2✔
286
                }
287
                codec.MergeHeader(w.Header(), header)
8✔
288
                w.WriteHeader(status)
8✔
289
                return
8✔
290
        }
291

292
        // 4xx and 5xx errors
293
        var rerr *reserr.Error
38✔
294
        if err == nil {
60✔
295
                rerr = statusError(status)
22✔
296
        } else {
38✔
297
                rerr = reserr.RESError(err)
16✔
298
        }
16✔
299

300
        codec.MergeHeader(w.Header(), header)
38✔
301
        w.Header().Set("Content-Type", enc.ContentType())
38✔
302
        w.WriteHeader(status)
38✔
303
        w.Write(enc.EncodeError(rerr))
38✔
304
}
305

306
func errorStatus(err error) (*reserr.Error, int) {
50✔
307
        rerr := reserr.RESError(err)
50✔
308

50✔
309
        var code int
50✔
310
        switch rerr.Code {
50✔
311
        case reserr.CodeNotFound:
1✔
312
                fallthrough
1✔
313
        case reserr.CodeMethodNotFound:
3✔
314
                fallthrough
3✔
315
        case reserr.CodeTimeout:
10✔
316
                code = http.StatusNotFound
10✔
317
        case reserr.CodeAccessDenied:
9✔
318
                code = http.StatusUnauthorized
9✔
319
        case reserr.CodeMethodNotAllowed:
6✔
320
                code = http.StatusMethodNotAllowed
6✔
321
        case reserr.CodeInternalError:
16✔
322
                code = http.StatusInternalServerError
16✔
323
        case reserr.CodeServiceUnavailable:
×
324
                code = http.StatusServiceUnavailable
×
UNCOV
325
        case reserr.CodeForbidden:
×
UNCOV
326
                code = http.StatusForbidden
×
327
        case reserr.CodeSubjectTooLong:
3✔
328
                code = http.StatusRequestURITooLong
3✔
329
        default:
6✔
330
                code = http.StatusBadRequest
6✔
331
        }
332

333
        return rerr, code
50✔
334
}
335

336
func httpError(w http.ResponseWriter, err error, enc APIEncoder) {
50✔
337
        rerr, code := errorStatus(err)
50✔
338
        w.Header().Set("Content-Type", enc.ContentType())
50✔
339
        w.WriteHeader(code)
50✔
340
        w.Write(enc.EncodeError(rerr))
50✔
341
}
50✔
342

343
// statusError returns a res error based on a HTTP status.
344
func statusError(status int) *reserr.Error {
22✔
345
        if status >= 400 {
44✔
346
                if status < 500 {
35✔
347
                        switch status {
13✔
348
                        // Access denied
349
                        case http.StatusUnauthorized:
1✔
350
                                fallthrough
1✔
351
                        case http.StatusPaymentRequired:
2✔
352
                                fallthrough
2✔
353
                        case http.StatusProxyAuthRequired:
3✔
354
                                return reserr.ErrAccessDenied
3✔
355

356
                        // Forbidden
357
                        case http.StatusForbidden:
1✔
358
                                fallthrough
1✔
359
                        case http.StatusUnavailableForLegalReasons:
2✔
360
                                return reserr.ErrForbidden
2✔
361

362
                        // Not found
363
                        case http.StatusGone:
1✔
364
                                fallthrough
1✔
365
                        case http.StatusNotFound:
2✔
366
                                return reserr.ErrNotFound
2✔
367

368
                        // Method not allowed
369
                        case http.StatusMethodNotAllowed:
1✔
370
                                return reserr.ErrMethodNotAllowed
1✔
371

372
                        // Timeout
373
                        case http.StatusRequestTimeout:
1✔
374
                                return reserr.ErrTimeout
1✔
375

376
                        // Bad request (default)
377
                        default:
4✔
378
                                return reserr.ErrBadRequest
4✔
379
                        }
380
                }
381
                if status < 600 {
18✔
382
                        switch status {
9✔
383
                        // Not implemented
384
                        case http.StatusNotImplemented:
1✔
385
                                return reserr.ErrNotImplemented
1✔
386

387
                        // Service unavailable
388
                        case http.StatusServiceUnavailable:
1✔
389
                                return reserr.ErrServiceUnavailable
1✔
390

391
                        // Timeout
392
                        case http.StatusGatewayTimeout:
1✔
393
                                return reserr.ErrTimeout
1✔
394

395
                        // Internal error (default)
396
                        default:
6✔
397
                                return reserr.ErrInternalError
6✔
398
                        }
399
                }
400
        }
401

402
        return reserr.ErrInternalError
×
403
}
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